diff --git a/.azdo/ci-pr.yaml b/.azdo/ci-pr.yaml index f0ccda57..f6b62566 100644 --- a/.azdo/ci-pr.yaml +++ b/.azdo/ci-pr.yaml @@ -28,7 +28,7 @@ steps: - script: | python -m pip install --upgrade pip python -m pip install flake8 pytest pytest-mock black pytest-asyncio build setuptools-git-versioning - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f dev_dependencies.txt ]; then pip install -r dev_dependencies.txt; fi displayName: 'Install dependencies' - script: | diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ad116932..4bba6227 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -30,8 +30,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest pytest-mock black pytest-asyncio build setuptools-git-versioning - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + python -m pip install flake8 black build setuptools-git-versioning + if [ -f dev_dependencies.txt ]; then pip install -r dev_dependencies.txt; fi - name: Check format with black run: | # stop the build if black raises an issue diff --git a/dev_dependencies.txt b/dev_dependencies.txt new file mode 100644 index 00000000..a90a4dcd --- /dev/null +++ b/dev_dependencies.txt @@ -0,0 +1,3 @@ +pytest +pytest-asyncio +pytest-mock \ No newline at end of file diff --git a/libraries/microsoft-agents-activity/tests/__init__.py b/tests/__init__.py similarity index 100% rename from libraries/microsoft-agents-activity/tests/__init__.py rename to tests/__init__.py diff --git a/libraries/microsoft-agents-activity/tests/activity_tools/__init__.py b/tests/_common/__init__.py similarity index 100% rename from libraries/microsoft-agents-activity/tests/activity_tools/__init__.py rename to tests/_common/__init__.py diff --git a/libraries/microsoft-agents-hosting-core/tests/core_tools/__init__.py b/tests/_common/storage/__init__.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/core_tools/__init__.py rename to tests/_common/storage/__init__.py diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/_storage_test_utils.py b/tests/_common/storage/utils.py similarity index 99% rename from libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/_storage_test_utils.py rename to tests/_common/storage/utils.py index 280f63e7..9a8f95ae 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/_storage_test_utils.py +++ b/tests/_common/storage/utils.py @@ -4,11 +4,12 @@ from abc import ABC from typing import Any -from .storage import Storage -from .store_item import StoreItem -from ._type_aliases import JSON -from .memory_storage import MemoryStorage - +from microsoft_agents.hosting.core.storage import ( + Storage, + StoreItem, + MemoryStorage +) +from microsoft_agents.hosting.core.storage._type_aliases import JSON class MockStoreItem(StoreItem): """Test implementation of StoreItem for testing purposes""" diff --git a/libraries/microsoft-agents-hosting-core/tests/tools/__init__.py b/tests/activity/__init__.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/tools/__init__.py rename to tests/activity/__init__.py diff --git a/libraries/microsoft-agents-activity/tests/activity_data/activity_test_data.py b/tests/activity/data/activity_test_data.py similarity index 100% rename from libraries/microsoft-agents-activity/tests/activity_data/activity_test_data.py rename to tests/activity/data/activity_test_data.py diff --git a/libraries/microsoft-agents-activity/tests/test_activity.py b/tests/activity/test_activity.py similarity index 99% rename from libraries/microsoft-agents-activity/tests/test_activity.py rename to tests/activity/test_activity.py index e18b4aad..44f42f4b 100644 --- a/libraries/microsoft-agents-activity/tests/test_activity.py +++ b/tests/activity/test_activity.py @@ -18,8 +18,8 @@ Thing, ) -from .activity_data.activity_test_data import MyChannelData -from .activity_tools.testing_activity import create_test_activity +from .data.activity_test_data import MyChannelData +from .tools.testing_activity import create_test_activity def helper_validate_recipient_and_from( diff --git a/libraries/microsoft-agents-activity/tests/test_token_response.py b/tests/activity/test_token_response.py similarity index 100% rename from libraries/microsoft-agents-activity/tests/test_token_response.py rename to tests/activity/test_token_response.py diff --git a/libraries/microsoft-agents-activity/tests/test_tools.py b/tests/activity/test_tools.py similarity index 97% rename from libraries/microsoft-agents-activity/tests/test_tools.py rename to tests/activity/test_tools.py index 47427b69..f50b88e5 100644 --- a/libraries/microsoft-agents-activity/tests/test_tools.py +++ b/tests/activity/test_tools.py @@ -9,7 +9,7 @@ pick_model_dict, ) -from .activity_tools.testing_model_utils import SkipFalse, SkipEmpty, PickField +from .tools.testing_model_utils import SkipFalse, SkipEmpty, PickField class TestModelUtils: diff --git a/tests/activity/tools/__init__.py b/tests/activity/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libraries/microsoft-agents-activity/tests/activity_tools/testing_activity.py b/tests/activity/tools/testing_activity.py similarity index 100% rename from libraries/microsoft-agents-activity/tests/activity_tools/testing_activity.py rename to tests/activity/tools/testing_activity.py diff --git a/libraries/microsoft-agents-activity/tests/activity_tools/testing_model_utils.py b/tests/activity/tools/testing_model_utils.py similarity index 100% rename from libraries/microsoft-agents-activity/tests/activity_tools/testing_model_utils.py rename to tests/activity/tools/testing_model_utils.py diff --git a/tests/authentication_msal/__init__.py b/tests/authentication_msal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py b/tests/authentication_msal/test_msal_auth.py similarity index 100% rename from libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py rename to tests/authentication_msal/test_msal_auth.py diff --git a/libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py b/tests/authentication_msal/test_msal_connection_manager.py similarity index 100% rename from libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py rename to tests/authentication_msal/test_msal_connection_manager.py diff --git a/tests/copilotstudio_client/__init__.py b/tests/copilotstudio_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hosting_aiohttp/__init__.py b/tests/hosting_aiohttp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hosting_core/__init__.py b/tests/hosting_core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libraries/microsoft-agents-hosting-core/tests/test_activity_handler.py b/tests/hosting_core/test_activity_handler.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/test_activity_handler.py rename to tests/hosting_core/test_activity_handler.py diff --git a/libraries/microsoft-agents-hosting-core/tests/test_agent_state.py b/tests/hosting_core/test_agent_state.py similarity index 99% rename from libraries/microsoft-agents-hosting-core/tests/test_agent_state.py rename to tests/hosting_core/test_agent_state.py index d88c2f55..5df89fed 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_agent_state.py +++ b/tests/hosting_core/test_agent_state.py @@ -25,7 +25,7 @@ ChannelAccount, ConversationAccount, ) -from core_tools.testing_adapter import TestingAdapter +from .tools.testing_adapter import TestingAdapter class MockCustomState(AgentState): diff --git a/libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py b/tests/hosting_core/test_auth_configuration.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py rename to tests/hosting_core/test_auth_configuration.py diff --git a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py b/tests/hosting_core/test_authorization.py similarity index 96% rename from libraries/microsoft-agents-hosting-core/tests/test_authorization.py rename to tests/hosting_core/test_authorization.py index f7956ec2..e2104cd5 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py +++ b/tests/hosting_core/test_authorization.py @@ -1,607 +1,608 @@ -import pytest - -import jwt - -from microsoft_agents.activity import ActivityTypes, TokenResponse -from microsoft_agents.hosting.core import MemoryStorage -from microsoft_agents.hosting.core.storage._storage_test_utils import StorageBaseline -from microsoft_agents.hosting.core.connector.user_token_base import UserTokenBase -from microsoft_agents.hosting.core.connector.user_token_client_base import ( - UserTokenClientBase, -) - -from microsoft_agents.hosting.core.app.oauth import Authorization -from microsoft_agents.hosting.core.oauth import ( - FlowStorageClient, - FlowErrorTag, - FlowStateTag, - FlowResponse, - OAuthFlow, -) - -# test constants -from core_tools.testing_oauth import * -from core_tools.testing_authorization import ( - TestingConnectionManager as MockConnectionManager, - create_test_auth_handler, -) - - -class TestUtils: - - def create_context( - self, - mocker, - channel_id="__channel_id", - user_id="__user_id", - user_token_client=None, - ): - - if not user_token_client: - user_token_client = self.create_mock_user_token_client(mocker) - - turn_context = mocker.Mock() - turn_context.activity.channel_id = channel_id - turn_context.activity.from_property.id = user_id - turn_context.activity.type = ActivityTypes.message - turn_context.adapter.USER_TOKEN_CLIENT_KEY = "__user_token_client" - turn_context.adapter.AGENT_IDENTITY_KEY = "__agent_identity_key" - agent_identity = mocker.Mock() - agent_identity.claims = {"aud": MS_APP_ID} - turn_context.turn_state = { - "__user_token_client": user_token_client, - "__agent_identity_key": agent_identity, - } - return turn_context - - def create_mock_user_token_client( - self, - mocker, - token=RES_TOKEN, - ): - mock_user_token_client_class = mocker.Mock(spec=UserTokenClientBase) - mock_user_token_client_class.user_token = mocker.Mock(spec=UserTokenBase) - mock_user_token_client_class.user_token.get_token = mocker.AsyncMock( - return_value=TokenResponse() if not token else TokenResponse(token=token) - ) - mock_user_token_client_class.user_token.sign_out = mocker.AsyncMock() - return mock_user_token_client_class - - @pytest.fixture - def mock_user_token_client_class(self, mocker): - return self.create_mock_user_token_client(mocker) - - def create_mock_oauth_flow_class(self, mocker, token_response): - mock_oauth_flow_class = mocker.Mock(spec=OAuthFlow) - # mock_oauth_flow_class.get_user_token = mocker.AsyncMock(return_value=token_response) - # mock_oauth_flow_class.sign_out = mocker.AsyncMock() - mocker.patch.object(OAuthFlow, "get_user_token", return_value=token_response) - mocker.patch.object(OAuthFlow, "sign_out") - return mock_oauth_flow_class - - @pytest.fixture - def mock_oauth_flow_class(self, mocker): - return self.create_mock_oauth_flow_class(mocker, TokenResponse(token=RES_TOKEN)) - # mock_flow_class = mocker.Mock(spec=OAuthFlow) - - # # mocker.patch.object(OAuthFlow, "__init__", return_value=mock_flow_class) - # mock_flow_class.get_user_token = mocker.AsyncMock(return_value=TokenResponse(token=RES_TOKEN)) - # mock_flow_class.sign_out = mocker.AsyncMock() - # mocker.patch.object(OAuthFlow, "get_user_token") - - # return mock_flow_class - - @pytest.fixture - def turn_context(self, mocker): - return self.create_context(mocker, "__channel_id", "__user_id", "__connection") - - def create_user_token_client(self, mocker, get_token_return=""): - - user_token_client = mocker.Mock(spec=UserTokenClientBase) - user_token_client.user_token = mocker.Mock(spec=UserTokenBase) - user_token_client.user_token.get_token = mocker.AsyncMock() - user_token_client.user_token.sign_out = mocker.AsyncMock() - - return_value = TokenResponse() - if isinstance(get_token_return, TokenResponse): - return_value = get_token_return - elif get_token_return: - return_value = TokenResponse(token=get_token_return) - user_token_client.user_token.get_token.return_value = return_value - - return user_token_client - - @pytest.fixture - def user_token_client(self, mocker): - return self.create_user_token_client(mocker, get_token_return=RES_TOKEN) - - @pytest.fixture - def auth_handlers(self): - handlers = {} - for key in STORAGE_INIT_DATA().keys(): - if key.startswith("auth/"): - auth_handler_name = key[key.rindex("/") + 1 :] - handlers[auth_handler_name] = create_test_auth_handler( - auth_handler_name, True - ) - return handlers - - @pytest.fixture - def connection_manager(self): - return MockConnectionManager() - - @pytest.fixture - def auth(self, connection_manager, storage, auth_handlers): - return Authorization(storage, connection_manager, auth_handlers) - - -class TestAuthorizationUtils(TestUtils): - - def create_storage(self): - return MemoryStorage(STORAGE_INIT_DATA()) - - @pytest.fixture - def storage(self): - return self.create_storage() - - @pytest.fixture - def baseline_storage(self): - return StorageBaseline(STORAGE_INIT_DATA()) - - def patch_flow( - self, - mocker, - flow_response=None, - token=None, - ): - mocker.patch.object( - OAuthFlow, "get_user_token", return_value=TokenResponse(token=token) - ) - mocker.patch.object(OAuthFlow, "sign_out") - mocker.patch.object( - OAuthFlow, "begin_or_continue_flow", return_value=flow_response - ) - - -class TestAuthorization(TestAuthorizationUtils): - - def test_init_configuration_variants( - self, storage, connection_manager, auth_handlers - ): - """Test initialization of authorization with different configuration variants.""" - AGENTAPPLICATION = { - "USERAUTHORIZATION": { - "HANDLERS": { - handler_name: { - "SETTINGS": { - "title": handler.title, - "text": handler.text, - "abs_oauth_connection_name": handler.abs_oauth_connection_name, - "obo_connection_name": handler.obo_connection_name, - } - } - for handler_name, handler in auth_handlers.items() - } - } - } - auth_with_config_obj = Authorization( - storage, - connection_manager, - auth_handlers=None, - AGENTAPPLICATION=AGENTAPPLICATION, - ) - auth_with_handlers_list = Authorization( - storage, connection_manager, auth_handlers=auth_handlers - ) - for auth_handler_name in auth_handlers.keys(): - auth_handler_a = auth_with_config_obj.resolve_handler(auth_handler_name) - auth_handler_b = auth_with_handlers_list.resolve_handler(auth_handler_name) - - assert auth_handler_a.name == auth_handler_b.name - assert auth_handler_a.title == auth_handler_b.title - assert auth_handler_a.text == auth_handler_b.text - assert ( - auth_handler_a.abs_oauth_connection_name - == auth_handler_b.abs_oauth_connection_name - ) - assert ( - auth_handler_a.obo_connection_name == auth_handler_b.obo_connection_name - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize( - "auth_handler_id, channel_id, user_id", - [["missing", "webchat", "Alice"], ["handler", "teams", "Bob"]], - ) - async def test_open_flow_value_error( - self, mocker, auth, auth_handler_id, channel_id, user_id - ): - """Test opening a flow with a missing auth handler.""" - context = self.create_context(mocker, channel_id, user_id) - with pytest.raises(ValueError): - async with auth.open_flow(context, auth_handler_id): - pass - - @pytest.mark.asyncio - @pytest.mark.parametrize( - "auth_handler_id, channel_id, user_id", - [ - ["", "webchat", "Alice"], - ["graph", "teams", "Bob"], - ["slack", "webchat", "Chuck"], - ], - ) - async def test_open_flow_readonly( - self, - mocker, - storage, - connection_manager, - auth_handlers, - auth_handler_id, - channel_id, - user_id, - ): - """Test opening a flow and not modifying it.""" - # setup - context = self.create_context(mocker, channel_id, user_id) - auth = Authorization(storage, connection_manager, auth_handlers) - flow_storage_client = FlowStorageClient(channel_id, user_id, storage) - - # test - async with auth.open_flow(context, auth_handler_id) as flow: - expected_flow_state = flow.flow_state - - # verify - actual_flow_state = await flow_storage_client.read( - auth.resolve_handler(auth_handler_id).name - ) - assert actual_flow_state == expected_flow_state - - @pytest.mark.asyncio - async def test_open_flow_success_modified_complete_flow( - self, - mocker, - storage, - connection_manager, - mock_user_token_client_class, - auth_handlers, - ): - # setup - channel_id = "teams" - user_id = "Alice" - auth_handler_id = "graph" - - self.create_user_token_client(mocker, get_token_return=RES_TOKEN) - - context = self.create_context(mocker, channel_id, user_id) - context.activity.type = ActivityTypes.message - context.activity.text = "123456" - - auth = Authorization(storage, connection_manager, auth_handlers) - flow_storage_client = FlowStorageClient(channel_id, user_id, storage) - - # test - async with auth.open_flow(context, auth_handler_id) as flow: - expected_flow_state = flow.flow_state - expected_flow_state.tag = FlowStateTag.COMPLETE - expected_flow_state.user_token = RES_TOKEN - - flow_response = await flow.begin_or_continue_flow(context.activity) - res_flow_state = flow_response.flow_state - - # verify - actual_flow_state = await flow_storage_client.read(auth_handler_id) - expected_flow_state.expiration = ( - res_flow_state.expiration - ) # we won't check this for now - - assert res_flow_state == expected_flow_state - assert actual_flow_state == expected_flow_state - - @pytest.mark.asyncio - async def test_open_flow_success_modified_failure( - self, - mocker, - storage, - connection_manager, - auth_handlers, - ): - # setup - channel_id = "teams" - user_id = "Bob" - auth_handler_id = "slack" - - context = self.create_context(mocker, channel_id, user_id) - context.activity.text = "invalid_magic_code" - - auth = Authorization(storage, connection_manager, auth_handlers) - flow_storage_client = FlowStorageClient(channel_id, user_id, storage) - - # test - async with auth.open_flow(context, auth_handler_id) as flow: - expected_flow_state = flow.flow_state - expected_flow_state.tag = FlowStateTag.FAILURE - expected_flow_state.attempts_remaining = 0 - - flow_response = await flow.begin_or_continue_flow(context.activity) - res_flow_state = flow_response.flow_state - - # verify - actual_flow_state = await flow_storage_client.read(auth_handler_id) - expected_flow_state.expiration = ( - actual_flow_state.expiration - ) # we won't check this for now - - assert flow_response.flow_error_tag == FlowErrorTag.MAGIC_FORMAT - assert res_flow_state == expected_flow_state - assert actual_flow_state == expected_flow_state - - @pytest.mark.asyncio - async def test_open_flow_success_modified_signout( - self, mocker, storage, connection_manager, auth_handlers - ): - # setup - channel_id = "webchat" - user_id = "Alice" - auth_handler_id = "graph" - - context = self.create_context(mocker, channel_id, user_id) - - auth = Authorization(storage, connection_manager, auth_handlers) - flow_storage_client = FlowStorageClient(channel_id, user_id, storage) - - # test - async with auth.open_flow(context, auth_handler_id) as flow: - expected_flow_state = flow.flow_state - expected_flow_state.tag = FlowStateTag.NOT_STARTED - expected_flow_state.user_token = "" - - await flow.sign_out() - - # verify - actual_flow_state = await flow_storage_client.read(auth_handler_id) - expected_flow_state.expiration = ( - actual_flow_state.expiration - ) # we won't check this for now - assert actual_flow_state == expected_flow_state - - @pytest.mark.asyncio - async def test_get_token_success(self, mocker, auth): - mock_user_token_client_class = self.create_user_token_client( - mocker, get_token_return=TokenResponse(token="token") - ) - context = self.create_context( - mocker, - "__channel_id", - "__user_id", - user_token_client=mock_user_token_client_class, - ) - assert await auth.get_token(context, "slack") == TokenResponse(token="token") - mock_user_token_client_class.user_token.get_token.assert_called_once() - - @pytest.mark.asyncio - async def test_get_token_empty_response(self, mocker, auth): - mock_user_token_client_class = self.create_user_token_client( - mocker, get_token_return=TokenResponse() - ) - context = self.create_context( - mocker, - "__channel_id", - "__user_id", - user_token_client=mock_user_token_client_class, - ) - assert await auth.get_token(context, "graph") == TokenResponse() - mock_user_token_client_class.user_token.get_token.assert_called_once() - - @pytest.mark.asyncio - async def test_get_token_error( - self, turn_context, storage, connection_manager, auth_handlers - ): - auth = Authorization(storage, connection_manager, auth_handlers) - with pytest.raises(ValueError): - await auth.get_token(turn_context, "missing-handler") - - @pytest.mark.asyncio - async def test_exchange_token_no_token(self, mocker, turn_context, auth): - self.create_mock_oauth_flow_class(mocker, TokenResponse()) - res = await auth.exchange_token(turn_context, ["scope"], "github") - assert res == TokenResponse() - - @pytest.mark.asyncio - async def test_exchange_token_not_exchangeable(self, mocker, turn_context, auth): - token = jwt.encode({"aud": "invalid://botframework.test.api"}, "") - self.create_mock_oauth_flow_class( - mocker, TokenResponse(connection_name="github", token=token) - ) - res = await auth.exchange_token(turn_context, ["scope"], "github") - assert res == TokenResponse() - - @pytest.mark.asyncio - async def test_exchange_token_valid_exchangeable(self, turn_context, mocker, auth): - token = jwt.encode({"aud": "api://botframework.test.api"}, "") - self.create_mock_oauth_flow_class( - mocker, TokenResponse(connection_name="github", token=token) - ) - mock_user_token_client_class = self.create_mock_user_token_client( - mocker, token=token - ) - mock_user_token_client_class.user_token.exchange_token = mocker.AsyncMock( - return_value=TokenResponse( - scopes=["scope"], token=token, connection_name="github" - ) - ) - res = await auth.exchange_token(turn_context, ["scope"], "github") - assert res == TokenResponse(token="github-obo-connection-obo-token") - - @pytest.mark.asyncio - async def test_get_active_flow_state(self, mocker, auth): - context = self.create_context(mocker, "webchat", "Alice") - actual_flow_state = await auth.get_active_flow_state(context) - assert ( - actual_flow_state - == STORAGE_SAMPLE_DICT[flow_key("webchat", "Alice", "github")] - ) - - @pytest.mark.asyncio - async def test_get_active_flow_state_missing(self, mocker, auth): - context = self.create_context(mocker, "__channel_id", "__user_id") - res = await auth.get_active_flow_state(context) - assert res is None - - @pytest.mark.asyncio - async def test_begin_or_continue_flow_success(self, mocker, auth): - # robrandao: TODO -> lower priority -> more testing here - # setup - mocker.patch.object( - OAuthFlow, - "begin_or_continue_flow", - return_value=FlowResponse( - token_response=TokenResponse(token="token"), - flow_state=FlowState( - tag=FlowStateTag.COMPLETE, auth_handler_id="github" - ), - ), - ) - context = self.create_context(mocker, "webchat", "Alice") - - context.dummy_val = None - - def on_sign_in_success(context, turn_state, auth_handler_id): - context.dummy_val = auth_handler_id - - def on_sign_in_failure(context, turn_state, auth_handler_id, err): - context.dummy_val = str(err) - - # test - auth.on_sign_in_success(on_sign_in_success) - auth.on_sign_in_failure(on_sign_in_failure) - flow_response = await auth.begin_or_continue_flow(context, None, "github") - assert context.dummy_val == "github" - assert flow_response.token_response == TokenResponse(token="token") - - @pytest.mark.asyncio - async def test_begin_or_continue_flow_already_completed(self, mocker, auth): - # robrandao: TODO -> lower priority -> more testing here - # setup - context = self.create_context(mocker, "webchat", "Alice") - - context.dummy_val = None - - def on_sign_in_success(context, turn_state, auth_handler_id): - context.dummy_val = auth_handler_id - - def on_sign_in_failure(context, turn_state, auth_handler_id, err): - context.dummy_val = str(err) - - # test - auth.on_sign_in_success(on_sign_in_success) - auth.on_sign_in_failure(on_sign_in_failure) - flow_response = await auth.begin_or_continue_flow(context, None, "graph") - assert context.dummy_val == None - assert flow_response.token_response == TokenResponse(token="test_token") - assert flow_response.continuation_activity is None - - @pytest.mark.asyncio - async def test_begin_or_continue_flow_failure( - self, mocker, mock_oauth_flow_class, auth - ): - # robrandao: TODO -> lower priority -> more testing here - # setup - mocker.patch.object( - OAuthFlow, - "begin_or_continue_flow", - return_value=FlowResponse( - token_response=TokenResponse(token="token"), - flow_state=FlowState( - tag=FlowStateTag.FAILURE, auth_handler_id="github" - ), - flow_state_error=FlowErrorTag.MAGIC_FORMAT, - ), - ) - context = self.create_context(mocker, "webchat", "Alice") - - context.dummy_val = None - - def on_sign_in_success(context, turn_state, auth_handler_id): - context.dummy_val = auth_handler_id - - def on_sign_in_failure(context, turn_state, auth_handler_id, err): - context.dummy_val = str(err) - - # test - auth.on_sign_in_success(on_sign_in_success) - auth.on_sign_in_failure(on_sign_in_failure) - flow_response = await auth.begin_or_continue_flow(context, None, "github") - assert context.dummy_val == "FlowErrorTag.NONE" - assert flow_response.token_response == TokenResponse(token="token") - - @pytest.mark.parametrize("auth_handler_id", ["graph", "github"]) - def test_resolve_handler_specified(self, auth, auth_handlers, auth_handler_id): - assert auth.resolve_handler(auth_handler_id) == auth_handlers[auth_handler_id] - - def test_resolve_handler_error(self, auth): - with pytest.raises(ValueError): - auth.resolve_handler("missing-handler") - - def test_resolve_handler_first(self, auth, auth_handlers): - assert auth.resolve_handler() == next(iter(auth_handlers.values())) - - @pytest.mark.asyncio - async def test_sign_out_individual( - self, - mocker, - mock_user_token_client_class, - mock_oauth_flow_class, - storage, - baseline_storage, - connection_manager, - auth_handlers, - ): - # setup - storage_client = FlowStorageClient("teams", "Alice", storage) - context = self.create_context(mocker, "teams", "Alice") - auth = Authorization(storage, connection_manager, auth_handlers) - - # test - await auth.sign_out(context, "graph") - - # verify - assert ( - await storage.read([storage_client.key("graph")], target_cls=FlowState) - == {} - ) - OAuthFlow.sign_out.assert_called_once() - - @pytest.mark.asyncio - async def test_sign_out_all( - self, - mocker, - mock_user_token_client_class, - mock_oauth_flow_class, - turn_context, - storage, - baseline_storage, - connection_manager, - auth_handlers, - ): - # setup - storage_client = FlowStorageClient("webchat", "Alice", storage) - - auth = Authorization(storage, connection_manager, auth_handlers) - context = self.create_context(mocker, "webchat", "Alice") - await auth.sign_out(context) - - # verify - assert ( - await storage.read([storage_client.key("graph")], target_cls=FlowState) - == {} - ) - assert ( - await storage.read([storage_client.key("github")], target_cls=FlowState) - == {} - ) - assert ( - await storage.read([storage_client.key("slack")], target_cls=FlowState) - == {} - ) - OAuthFlow.sign_out.assert_called() # ignore red squiggly -> mocked +import pytest + +import jwt + +from microsoft_agents.activity import ActivityTypes, TokenResponse +from microsoft_agents.hosting.core import MemoryStorage +from microsoft_agents.hosting.core.connector.user_token_base import UserTokenBase +from microsoft_agents.hosting.core.connector.user_token_client_base import ( + UserTokenClientBase, +) + +from microsoft_agents.hosting.core.app.oauth import Authorization +from microsoft_agents.hosting.core.oauth import ( + FlowStorageClient, + FlowErrorTag, + FlowStateTag, + FlowResponse, + OAuthFlow, +) + +from tests._common.storage.utils import StorageBaseline + +# test constants +from .tools.testing_oauth import * +from .tools.testing_authorization import ( + TestingConnectionManager as MockConnectionManager, + create_test_auth_handler, +) + + +class TestUtils: + + def create_context( + self, + mocker, + channel_id="__channel_id", + user_id="__user_id", + user_token_client=None, + ): + + if not user_token_client: + user_token_client = self.create_mock_user_token_client(mocker) + + turn_context = mocker.Mock() + turn_context.activity.channel_id = channel_id + turn_context.activity.from_property.id = user_id + turn_context.activity.type = ActivityTypes.message + turn_context.adapter.USER_TOKEN_CLIENT_KEY = "__user_token_client" + turn_context.adapter.AGENT_IDENTITY_KEY = "__agent_identity_key" + agent_identity = mocker.Mock() + agent_identity.claims = {"aud": MS_APP_ID} + turn_context.turn_state = { + "__user_token_client": user_token_client, + "__agent_identity_key": agent_identity, + } + return turn_context + + def create_mock_user_token_client( + self, + mocker, + token=RES_TOKEN, + ): + mock_user_token_client_class = mocker.Mock(spec=UserTokenClientBase) + mock_user_token_client_class.user_token = mocker.Mock(spec=UserTokenBase) + mock_user_token_client_class.user_token.get_token = mocker.AsyncMock( + return_value=TokenResponse() if not token else TokenResponse(token=token) + ) + mock_user_token_client_class.user_token.sign_out = mocker.AsyncMock() + return mock_user_token_client_class + + @pytest.fixture + def mock_user_token_client_class(self, mocker): + return self.create_mock_user_token_client(mocker) + + def create_mock_oauth_flow_class(self, mocker, token_response): + mock_oauth_flow_class = mocker.Mock(spec=OAuthFlow) + # mock_oauth_flow_class.get_user_token = mocker.AsyncMock(return_value=token_response) + # mock_oauth_flow_class.sign_out = mocker.AsyncMock() + mocker.patch.object(OAuthFlow, "get_user_token", return_value=token_response) + mocker.patch.object(OAuthFlow, "sign_out") + return mock_oauth_flow_class + + @pytest.fixture + def mock_oauth_flow_class(self, mocker): + return self.create_mock_oauth_flow_class(mocker, TokenResponse(token=RES_TOKEN)) + # mock_flow_class = mocker.Mock(spec=OAuthFlow) + + # # mocker.patch.object(OAuthFlow, "__init__", return_value=mock_flow_class) + # mock_flow_class.get_user_token = mocker.AsyncMock(return_value=TokenResponse(token=RES_TOKEN)) + # mock_flow_class.sign_out = mocker.AsyncMock() + # mocker.patch.object(OAuthFlow, "get_user_token") + + # return mock_flow_class + + @pytest.fixture + def turn_context(self, mocker): + return self.create_context(mocker, "__channel_id", "__user_id", "__connection") + + def create_user_token_client(self, mocker, get_token_return=""): + + user_token_client = mocker.Mock(spec=UserTokenClientBase) + user_token_client.user_token = mocker.Mock(spec=UserTokenBase) + user_token_client.user_token.get_token = mocker.AsyncMock() + user_token_client.user_token.sign_out = mocker.AsyncMock() + + return_value = TokenResponse() + if isinstance(get_token_return, TokenResponse): + return_value = get_token_return + elif get_token_return: + return_value = TokenResponse(token=get_token_return) + user_token_client.user_token.get_token.return_value = return_value + + return user_token_client + + @pytest.fixture + def user_token_client(self, mocker): + return self.create_user_token_client(mocker, get_token_return=RES_TOKEN) + + @pytest.fixture + def auth_handlers(self): + handlers = {} + for key in STORAGE_INIT_DATA().keys(): + if key.startswith("auth/"): + auth_handler_name = key[key.rindex("/") + 1 :] + handlers[auth_handler_name] = create_test_auth_handler( + auth_handler_name, True + ) + return handlers + + @pytest.fixture + def connection_manager(self): + return MockConnectionManager() + + @pytest.fixture + def auth(self, connection_manager, storage, auth_handlers): + return Authorization(storage, connection_manager, auth_handlers) + + +class TestAuthorizationUtils(TestUtils): + + def create_storage(self): + return MemoryStorage(STORAGE_INIT_DATA()) + + @pytest.fixture + def storage(self): + return self.create_storage() + + @pytest.fixture + def baseline_storage(self): + return StorageBaseline(STORAGE_INIT_DATA()) + + def patch_flow( + self, + mocker, + flow_response=None, + token=None, + ): + mocker.patch.object( + OAuthFlow, "get_user_token", return_value=TokenResponse(token=token) + ) + mocker.patch.object(OAuthFlow, "sign_out") + mocker.patch.object( + OAuthFlow, "begin_or_continue_flow", return_value=flow_response + ) + + +class TestAuthorization(TestAuthorizationUtils): + + def test_init_configuration_variants( + self, storage, connection_manager, auth_handlers + ): + """Test initialization of authorization with different configuration variants.""" + AGENTAPPLICATION = { + "USERAUTHORIZATION": { + "HANDLERS": { + handler_name: { + "SETTINGS": { + "title": handler.title, + "text": handler.text, + "abs_oauth_connection_name": handler.abs_oauth_connection_name, + "obo_connection_name": handler.obo_connection_name, + } + } + for handler_name, handler in auth_handlers.items() + } + } + } + auth_with_config_obj = Authorization( + storage, + connection_manager, + auth_handlers=None, + AGENTAPPLICATION=AGENTAPPLICATION, + ) + auth_with_handlers_list = Authorization( + storage, connection_manager, auth_handlers=auth_handlers + ) + for auth_handler_name in auth_handlers.keys(): + auth_handler_a = auth_with_config_obj.resolve_handler(auth_handler_name) + auth_handler_b = auth_with_handlers_list.resolve_handler(auth_handler_name) + + assert auth_handler_a.name == auth_handler_b.name + assert auth_handler_a.title == auth_handler_b.title + assert auth_handler_a.text == auth_handler_b.text + assert ( + auth_handler_a.abs_oauth_connection_name + == auth_handler_b.abs_oauth_connection_name + ) + assert ( + auth_handler_a.obo_connection_name == auth_handler_b.obo_connection_name + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "auth_handler_id, channel_id, user_id", + [["missing", "webchat", "Alice"], ["handler", "teams", "Bob"]], + ) + async def test_open_flow_value_error( + self, mocker, auth, auth_handler_id, channel_id, user_id + ): + """Test opening a flow with a missing auth handler.""" + context = self.create_context(mocker, channel_id, user_id) + with pytest.raises(ValueError): + async with auth.open_flow(context, auth_handler_id): + pass + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "auth_handler_id, channel_id, user_id", + [ + ["", "webchat", "Alice"], + ["graph", "teams", "Bob"], + ["slack", "webchat", "Chuck"], + ], + ) + async def test_open_flow_readonly( + self, + mocker, + storage, + connection_manager, + auth_handlers, + auth_handler_id, + channel_id, + user_id, + ): + """Test opening a flow and not modifying it.""" + # setup + context = self.create_context(mocker, channel_id, user_id) + auth = Authorization(storage, connection_manager, auth_handlers) + flow_storage_client = FlowStorageClient(channel_id, user_id, storage) + + # test + async with auth.open_flow(context, auth_handler_id) as flow: + expected_flow_state = flow.flow_state + + # verify + actual_flow_state = await flow_storage_client.read( + auth.resolve_handler(auth_handler_id).name + ) + assert actual_flow_state == expected_flow_state + + @pytest.mark.asyncio + async def test_open_flow_success_modified_complete_flow( + self, + mocker, + storage, + connection_manager, + mock_user_token_client_class, + auth_handlers, + ): + # setup + channel_id = "teams" + user_id = "Alice" + auth_handler_id = "graph" + + self.create_user_token_client(mocker, get_token_return=RES_TOKEN) + + context = self.create_context(mocker, channel_id, user_id) + context.activity.type = ActivityTypes.message + context.activity.text = "123456" + + auth = Authorization(storage, connection_manager, auth_handlers) + flow_storage_client = FlowStorageClient(channel_id, user_id, storage) + + # test + async with auth.open_flow(context, auth_handler_id) as flow: + expected_flow_state = flow.flow_state + expected_flow_state.tag = FlowStateTag.COMPLETE + expected_flow_state.user_token = RES_TOKEN + + flow_response = await flow.begin_or_continue_flow(context.activity) + res_flow_state = flow_response.flow_state + + # verify + actual_flow_state = await flow_storage_client.read(auth_handler_id) + expected_flow_state.expiration = ( + res_flow_state.expiration + ) # we won't check this for now + + assert res_flow_state == expected_flow_state + assert actual_flow_state == expected_flow_state + + @pytest.mark.asyncio + async def test_open_flow_success_modified_failure( + self, + mocker, + storage, + connection_manager, + auth_handlers, + ): + # setup + channel_id = "teams" + user_id = "Bob" + auth_handler_id = "slack" + + context = self.create_context(mocker, channel_id, user_id) + context.activity.text = "invalid_magic_code" + + auth = Authorization(storage, connection_manager, auth_handlers) + flow_storage_client = FlowStorageClient(channel_id, user_id, storage) + + # test + async with auth.open_flow(context, auth_handler_id) as flow: + expected_flow_state = flow.flow_state + expected_flow_state.tag = FlowStateTag.FAILURE + expected_flow_state.attempts_remaining = 0 + + flow_response = await flow.begin_or_continue_flow(context.activity) + res_flow_state = flow_response.flow_state + + # verify + actual_flow_state = await flow_storage_client.read(auth_handler_id) + expected_flow_state.expiration = ( + actual_flow_state.expiration + ) # we won't check this for now + + assert flow_response.flow_error_tag == FlowErrorTag.MAGIC_FORMAT + assert res_flow_state == expected_flow_state + assert actual_flow_state == expected_flow_state + + @pytest.mark.asyncio + async def test_open_flow_success_modified_signout( + self, mocker, storage, connection_manager, auth_handlers + ): + # setup + channel_id = "webchat" + user_id = "Alice" + auth_handler_id = "graph" + + context = self.create_context(mocker, channel_id, user_id) + + auth = Authorization(storage, connection_manager, auth_handlers) + flow_storage_client = FlowStorageClient(channel_id, user_id, storage) + + # test + async with auth.open_flow(context, auth_handler_id) as flow: + expected_flow_state = flow.flow_state + expected_flow_state.tag = FlowStateTag.NOT_STARTED + expected_flow_state.user_token = "" + + await flow.sign_out() + + # verify + actual_flow_state = await flow_storage_client.read(auth_handler_id) + expected_flow_state.expiration = ( + actual_flow_state.expiration + ) # we won't check this for now + assert actual_flow_state == expected_flow_state + + @pytest.mark.asyncio + async def test_get_token_success(self, mocker, auth): + mock_user_token_client_class = self.create_user_token_client( + mocker, get_token_return=TokenResponse(token="token") + ) + context = self.create_context( + mocker, + "__channel_id", + "__user_id", + user_token_client=mock_user_token_client_class, + ) + assert await auth.get_token(context, "slack") == TokenResponse(token="token") + mock_user_token_client_class.user_token.get_token.assert_called_once() + + @pytest.mark.asyncio + async def test_get_token_empty_response(self, mocker, auth): + mock_user_token_client_class = self.create_user_token_client( + mocker, get_token_return=TokenResponse() + ) + context = self.create_context( + mocker, + "__channel_id", + "__user_id", + user_token_client=mock_user_token_client_class, + ) + assert await auth.get_token(context, "graph") == TokenResponse() + mock_user_token_client_class.user_token.get_token.assert_called_once() + + @pytest.mark.asyncio + async def test_get_token_error( + self, turn_context, storage, connection_manager, auth_handlers + ): + auth = Authorization(storage, connection_manager, auth_handlers) + with pytest.raises(ValueError): + await auth.get_token(turn_context, "missing-handler") + + @pytest.mark.asyncio + async def test_exchange_token_no_token(self, mocker, turn_context, auth): + self.create_mock_oauth_flow_class(mocker, TokenResponse()) + res = await auth.exchange_token(turn_context, ["scope"], "github") + assert res == TokenResponse() + + @pytest.mark.asyncio + async def test_exchange_token_not_exchangeable(self, mocker, turn_context, auth): + token = jwt.encode({"aud": "invalid://botframework.test.api"}, "") + self.create_mock_oauth_flow_class( + mocker, TokenResponse(connection_name="github", token=token) + ) + res = await auth.exchange_token(turn_context, ["scope"], "github") + assert res == TokenResponse() + + @pytest.mark.asyncio + async def test_exchange_token_valid_exchangeable(self, turn_context, mocker, auth): + token = jwt.encode({"aud": "api://botframework.test.api"}, "") + self.create_mock_oauth_flow_class( + mocker, TokenResponse(connection_name="github", token=token) + ) + mock_user_token_client_class = self.create_mock_user_token_client( + mocker, token=token + ) + mock_user_token_client_class.user_token.exchange_token = mocker.AsyncMock( + return_value=TokenResponse( + scopes=["scope"], token=token, connection_name="github" + ) + ) + res = await auth.exchange_token(turn_context, ["scope"], "github") + assert res == TokenResponse(token="github-obo-connection-obo-token") + + @pytest.mark.asyncio + async def test_get_active_flow_state(self, mocker, auth): + context = self.create_context(mocker, "webchat", "Alice") + actual_flow_state = await auth.get_active_flow_state(context) + assert ( + actual_flow_state + == STORAGE_SAMPLE_DICT[flow_key("webchat", "Alice", "github")] + ) + + @pytest.mark.asyncio + async def test_get_active_flow_state_missing(self, mocker, auth): + context = self.create_context(mocker, "__channel_id", "__user_id") + res = await auth.get_active_flow_state(context) + assert res is None + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_success(self, mocker, auth): + # robrandao: TODO -> lower priority -> more testing here + # setup + mocker.patch.object( + OAuthFlow, + "begin_or_continue_flow", + return_value=FlowResponse( + token_response=TokenResponse(token="token"), + flow_state=FlowState( + tag=FlowStateTag.COMPLETE, auth_handler_id="github" + ), + ), + ) + context = self.create_context(mocker, "webchat", "Alice") + + context.dummy_val = None + + def on_sign_in_success(context, turn_state, auth_handler_id): + context.dummy_val = auth_handler_id + + def on_sign_in_failure(context, turn_state, auth_handler_id, err): + context.dummy_val = str(err) + + # test + auth.on_sign_in_success(on_sign_in_success) + auth.on_sign_in_failure(on_sign_in_failure) + flow_response = await auth.begin_or_continue_flow(context, None, "github") + assert context.dummy_val == "github" + assert flow_response.token_response == TokenResponse(token="token") + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_already_completed(self, mocker, auth): + # robrandao: TODO -> lower priority -> more testing here + # setup + context = self.create_context(mocker, "webchat", "Alice") + + context.dummy_val = None + + def on_sign_in_success(context, turn_state, auth_handler_id): + context.dummy_val = auth_handler_id + + def on_sign_in_failure(context, turn_state, auth_handler_id, err): + context.dummy_val = str(err) + + # test + auth.on_sign_in_success(on_sign_in_success) + auth.on_sign_in_failure(on_sign_in_failure) + flow_response = await auth.begin_or_continue_flow(context, None, "graph") + assert context.dummy_val == None + assert flow_response.token_response == TokenResponse(token="test_token") + assert flow_response.continuation_activity is None + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_failure( + self, mocker, mock_oauth_flow_class, auth + ): + # robrandao: TODO -> lower priority -> more testing here + # setup + mocker.patch.object( + OAuthFlow, + "begin_or_continue_flow", + return_value=FlowResponse( + token_response=TokenResponse(token="token"), + flow_state=FlowState( + tag=FlowStateTag.FAILURE, auth_handler_id="github" + ), + flow_state_error=FlowErrorTag.MAGIC_FORMAT, + ), + ) + context = self.create_context(mocker, "webchat", "Alice") + + context.dummy_val = None + + def on_sign_in_success(context, turn_state, auth_handler_id): + context.dummy_val = auth_handler_id + + def on_sign_in_failure(context, turn_state, auth_handler_id, err): + context.dummy_val = str(err) + + # test + auth.on_sign_in_success(on_sign_in_success) + auth.on_sign_in_failure(on_sign_in_failure) + flow_response = await auth.begin_or_continue_flow(context, None, "github") + assert context.dummy_val == "FlowErrorTag.NONE" + assert flow_response.token_response == TokenResponse(token="token") + + @pytest.mark.parametrize("auth_handler_id", ["graph", "github"]) + def test_resolve_handler_specified(self, auth, auth_handlers, auth_handler_id): + assert auth.resolve_handler(auth_handler_id) == auth_handlers[auth_handler_id] + + def test_resolve_handler_error(self, auth): + with pytest.raises(ValueError): + auth.resolve_handler("missing-handler") + + def test_resolve_handler_first(self, auth, auth_handlers): + assert auth.resolve_handler() == next(iter(auth_handlers.values())) + + @pytest.mark.asyncio + async def test_sign_out_individual( + self, + mocker, + mock_user_token_client_class, + mock_oauth_flow_class, + storage, + baseline_storage, + connection_manager, + auth_handlers, + ): + # setup + storage_client = FlowStorageClient("teams", "Alice", storage) + context = self.create_context(mocker, "teams", "Alice") + auth = Authorization(storage, connection_manager, auth_handlers) + + # test + await auth.sign_out(context, "graph") + + # verify + assert ( + await storage.read([storage_client.key("graph")], target_cls=FlowState) + == {} + ) + OAuthFlow.sign_out.assert_called_once() + + @pytest.mark.asyncio + async def test_sign_out_all( + self, + mocker, + mock_user_token_client_class, + mock_oauth_flow_class, + turn_context, + storage, + baseline_storage, + connection_manager, + auth_handlers, + ): + # setup + storage_client = FlowStorageClient("webchat", "Alice", storage) + + auth = Authorization(storage, connection_manager, auth_handlers) + context = self.create_context(mocker, "webchat", "Alice") + await auth.sign_out(context) + + # verify + assert ( + await storage.read([storage_client.key("graph")], target_cls=FlowState) + == {} + ) + assert ( + await storage.read([storage_client.key("github")], target_cls=FlowState) + == {} + ) + assert ( + await storage.read([storage_client.key("slack")], target_cls=FlowState) + == {} + ) + OAuthFlow.sign_out.assert_called() # ignore red squiggly -> mocked diff --git a/libraries/microsoft-agents-hosting-core/tests/test_error_handling.py b/tests/hosting_core/test_error_handling.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/test_error_handling.py rename to tests/hosting_core/test_error_handling.py diff --git a/libraries/microsoft-agents-hosting-core/tests/test_flow_state.py b/tests/hosting_core/test_flow_state.py similarity index 96% rename from libraries/microsoft-agents-hosting-core/tests/test_flow_state.py rename to tests/hosting_core/test_flow_state.py index 1b17b939..3d12f48c 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_flow_state.py +++ b/tests/hosting_core/test_flow_state.py @@ -1,237 +1,237 @@ -from datetime import datetime - -import pytest - -from microsoft_agents.hosting.core.oauth.flow_state import FlowState, FlowStateTag - - -class TestFlowState: - - @pytest.mark.parametrize( - "original_flow_state, refresh_to_not_started", - [ - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=0, - expiration=datetime.now().timestamp(), - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.BEGIN, - attempts_remaining=1, - expiration=datetime.now().timestamp(), - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.COMPLETE, - attempts_remaining=0, - expiration=datetime.now().timestamp() - 100, - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=1, - expiration=datetime.now().timestamp() + 1000, - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.FAILURE, - attempts_remaining=-1, - expiration=datetime.now().timestamp(), - ), - False, - ), - ], - ) - def test_refresh(self, original_flow_state, refresh_to_not_started): - new_flow_state = original_flow_state.model_copy() - new_flow_state.refresh() - expected_flow_state = original_flow_state.model_copy() - if refresh_to_not_started: - expected_flow_state.tag = FlowStateTag.NOT_STARTED - assert new_flow_state == expected_flow_state - - @pytest.mark.parametrize( - "flow_state, expected", - [ - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=0, - expiration=datetime.now().timestamp(), - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.BEGIN, - attempts_remaining=1, - expiration=datetime.now().timestamp(), - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.COMPLETE, - attempts_remaining=0, - expiration=datetime.now().timestamp() - 100, - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=1, - expiration=datetime.now().timestamp() + 1000, - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.FAILURE, - attempts_remaining=-1, - expiration=datetime.now().timestamp() + 1000, - ), - False, - ), - ], - ) - def test_is_expired(self, flow_state, expected): - assert flow_state.is_expired() == expected - - @pytest.mark.parametrize( - "flow_state, expected", - [ - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=0, - expiration=datetime.now().timestamp(), - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.BEGIN, - attempts_remaining=1, - expiration=datetime.now().timestamp(), - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.COMPLETE, - attempts_remaining=0, - expiration=datetime.now().timestamp() - 100, - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=1, - expiration=datetime.now().timestamp() - 100, - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.FAILURE, - attempts_remaining=-1, - expiration=datetime.now().timestamp(), - ), - True, - ), - ], - ) - def test_reached_max_attempts(self, flow_state, expected): - assert flow_state.reached_max_attempts() == expected - - @pytest.mark.parametrize( - "flow_state, expected", - [ - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=0, - expiration=datetime.now().timestamp(), - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.BEGIN, - attempts_remaining=1, - expiration=datetime.now().timestamp(), - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.COMPLETE, - attempts_remaining=0, - expiration=datetime.now().timestamp() - 100, - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.FAILURE, - attempts_remaining=1, - expiration=datetime.now().timestamp() - 100, - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=2, - expiration=datetime.now().timestamp() + 1000, - ), - True, - ), - ( - FlowState( - tag=FlowStateTag.BEGIN, - attempts_remaining=0, - expiration=datetime.now().timestamp() + 1000, - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.COMPLETE, - attempts_remaining=-1, - expiration=datetime.now().timestamp() + 1000, - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.FAILURE, - attempts_remaining=1, - expiration=datetime.now().timestamp() + 1000, - ), - False, - ), - ( - FlowState( - tag=FlowStateTag.CONTINUE, - attempts_remaining=1, - expiration=datetime.now().timestamp() + 1000, - ), - True, - ), - ], - ) - def test_is_active(self, flow_state, expected): - assert flow_state.is_active() == expected +from datetime import datetime + +import pytest + +from microsoft_agents.hosting.core.oauth.flow_state import FlowState, FlowStateTag + + +class TestFlowState: + + @pytest.mark.parametrize( + "original_flow_state, refresh_to_not_started", + [ + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=0, + expiration=datetime.now().timestamp(), + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.BEGIN, + attempts_remaining=1, + expiration=datetime.now().timestamp(), + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.COMPLETE, + attempts_remaining=0, + expiration=datetime.now().timestamp() - 100, + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=1, + expiration=datetime.now().timestamp() + 1000, + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.FAILURE, + attempts_remaining=-1, + expiration=datetime.now().timestamp(), + ), + False, + ), + ], + ) + def test_refresh(self, original_flow_state, refresh_to_not_started): + new_flow_state = original_flow_state.model_copy() + new_flow_state.refresh() + expected_flow_state = original_flow_state.model_copy() + if refresh_to_not_started: + expected_flow_state.tag = FlowStateTag.NOT_STARTED + assert new_flow_state == expected_flow_state + + @pytest.mark.parametrize( + "flow_state, expected", + [ + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=0, + expiration=datetime.now().timestamp(), + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.BEGIN, + attempts_remaining=1, + expiration=datetime.now().timestamp(), + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.COMPLETE, + attempts_remaining=0, + expiration=datetime.now().timestamp() - 100, + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=1, + expiration=datetime.now().timestamp() + 1000, + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.FAILURE, + attempts_remaining=-1, + expiration=datetime.now().timestamp() + 1000, + ), + False, + ), + ], + ) + def test_is_expired(self, flow_state, expected): + assert flow_state.is_expired() == expected + + @pytest.mark.parametrize( + "flow_state, expected", + [ + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=0, + expiration=datetime.now().timestamp(), + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.BEGIN, + attempts_remaining=1, + expiration=datetime.now().timestamp(), + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.COMPLETE, + attempts_remaining=0, + expiration=datetime.now().timestamp() - 100, + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=1, + expiration=datetime.now().timestamp() - 100, + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.FAILURE, + attempts_remaining=-1, + expiration=datetime.now().timestamp(), + ), + True, + ), + ], + ) + def test_reached_max_attempts(self, flow_state, expected): + assert flow_state.reached_max_attempts() == expected + + @pytest.mark.parametrize( + "flow_state, expected", + [ + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=0, + expiration=datetime.now().timestamp(), + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.BEGIN, + attempts_remaining=1, + expiration=datetime.now().timestamp(), + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.COMPLETE, + attempts_remaining=0, + expiration=datetime.now().timestamp() - 100, + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.FAILURE, + attempts_remaining=1, + expiration=datetime.now().timestamp() - 100, + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=2, + expiration=datetime.now().timestamp() + 1000, + ), + True, + ), + ( + FlowState( + tag=FlowStateTag.BEGIN, + attempts_remaining=0, + expiration=datetime.now().timestamp() + 1000, + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.COMPLETE, + attempts_remaining=-1, + expiration=datetime.now().timestamp() + 1000, + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.FAILURE, + attempts_remaining=1, + expiration=datetime.now().timestamp() + 1000, + ), + False, + ), + ( + FlowState( + tag=FlowStateTag.CONTINUE, + attempts_remaining=1, + expiration=datetime.now().timestamp() + 1000, + ), + True, + ), + ], + ) + def test_is_active(self, flow_state, expected): + assert flow_state.is_active() == expected diff --git a/libraries/microsoft-agents-hosting-core/tests/test_flow_storage_client.py b/tests/hosting_core/test_flow_storage_client.py similarity index 96% rename from libraries/microsoft-agents-hosting-core/tests/test_flow_storage_client.py rename to tests/hosting_core/test_flow_storage_client.py index b93d7a0d..090850d3 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_flow_storage_client.py +++ b/tests/hosting_core/test_flow_storage_client.py @@ -1,170 +1,171 @@ -import pytest - -from microsoft_agents.hosting.core.storage import MemoryStorage -from microsoft_agents.hosting.core.storage._storage_test_utils import MockStoreItem -from microsoft_agents.hosting.core.oauth import FlowState, FlowStorageClient - - -class TestFlowStorageClient: - - @pytest.fixture - def channel_id(self): - return "__channel_id" - - @pytest.fixture - def user_id(self): - return "__user_id" - - @pytest.fixture - def storage(self): - return MemoryStorage() - - @pytest.fixture - def client(self, channel_id, user_id, storage): - return FlowStorageClient(channel_id, user_id, storage) - - @pytest.mark.asyncio - @pytest.mark.parametrize( - "channel_id, user_id", - [ - ("channel_id", "user_id"), - ("teams_id", "Bob"), - ("channel", "Alice"), - ], - ) - async def test_init_base_key(self, mocker, channel_id, user_id): - client = FlowStorageClient(channel_id, user_id, mocker.Mock()) - assert client.base_key == f"auth/{channel_id}/{user_id}/" - - @pytest.mark.asyncio - async def test_init_fails_without_user_id(self, channel_id, storage): - with pytest.raises(ValueError): - FlowStorageClient(channel_id, "", storage) - - @pytest.mark.asyncio - async def test_init_fails_without_channel_id(self, user_id, storage): - with pytest.raises(ValueError): - FlowStorageClient("", user_id, storage) - - @pytest.mark.parametrize( - "auth_handler_id, expected", - [ - ("handler", "auth/__channel_id/__user_id/handler"), - ("auth_handler", "auth/__channel_id/__user_id/auth_handler"), - ], - ) - def test_key(self, client, auth_handler_id, expected): - assert client.key(auth_handler_id) == expected - - @pytest.mark.asyncio - @pytest.mark.parametrize("auth_handler_id", ["handler", "auth_handler"]) - async def test_read(self, mocker, user_id, channel_id, auth_handler_id): - storage = mocker.AsyncMock() - key = f"auth/{channel_id}/{user_id}/{auth_handler_id}" - storage.read.return_value = {key: FlowState()} - client = FlowStorageClient(channel_id, user_id, storage) - res = await client.read(auth_handler_id) - assert res is storage.read.return_value[key] - storage.read.assert_called_once_with( - [client.key(auth_handler_id)], target_cls=FlowState - ) - - @pytest.mark.asyncio - async def test_read_missing(self, mocker): - storage = mocker.AsyncMock() - storage.read.return_value = {} - client = FlowStorageClient("__channel_id", "__user_id", storage) - res = await client.read("non_existent_handler") - assert res is None - storage.read.assert_called_once_with( - [client.key("non_existent_handler")], target_cls=FlowState - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize("auth_handler_id", ["handler", "auth_handler"]) - async def test_write(self, mocker, channel_id, user_id, auth_handler_id): - storage = mocker.AsyncMock() - storage.write.return_value = None - client = FlowStorageClient(channel_id, user_id, storage) - flow_state = mocker.Mock(spec=FlowState) - flow_state.auth_handler_id = auth_handler_id - await client.write(flow_state) - storage.write.assert_called_once_with({client.key(auth_handler_id): flow_state}) - - @pytest.mark.asyncio - @pytest.mark.parametrize("auth_handler_id", ["handler", "auth_handler"]) - async def test_delete(self, mocker, channel_id, user_id, auth_handler_id): - storage = mocker.AsyncMock() - storage.delete.return_value = None - client = FlowStorageClient(channel_id, user_id, storage) - await client.delete(auth_handler_id) - storage.delete.assert_called_once_with([client.key(auth_handler_id)]) - - @pytest.mark.asyncio - async def test_integration_with_memory_storage(self, channel_id, user_id): - - flow_state_alpha = FlowState(auth_handler_id="handler", flow_started=True) - flow_state_beta = FlowState( - auth_handler_id="auth_handler", flow_started=True, user_token="token" - ) - - storage = MemoryStorage( - { - "some_data": MockStoreItem({"value": "test"}), - f"auth/{channel_id}/{user_id}/handler": flow_state_alpha, - f"auth/{channel_id}/{user_id}/auth_handler": flow_state_beta, - } - ) - baseline = MemoryStorage( - { - "some_data": MockStoreItem({"value": "test"}), - f"auth/{channel_id}/{user_id}/handler": flow_state_alpha, - f"fauth/{channel_id}/{user_id}/auth_handler": flow_state_beta, - } - ) - - # helpers - async def read_check(*args, **kwargs): - res_storage = await storage.read(*args, **kwargs) - res_baseline = await baseline.read(*args, **kwargs) - assert res_storage == res_baseline - - async def write_both(*args, **kwargs): - await storage.write(*args, **kwargs) - await baseline.write(*args, **kwargs) - - async def delete_both(*args, **kwargs): - await storage.delete(*args, **kwargs) - await baseline.delete(*args, **kwargs) - - client = FlowStorageClient(channel_id, user_id, storage) - - new_flow_state_alpha = FlowState(auth_handler_id="handler") - flow_state_chi = FlowState(auth_handler_id="chi") - - await client.write(new_flow_state_alpha) - await client.write(flow_state_chi) - await baseline.write( - {f"auth/{channel_id}/{user_id}/handler": new_flow_state_alpha.model_copy()} - ) - await baseline.write( - {f"auth/{channel_id}/{user_id}/chi": flow_state_chi.model_copy()} - ) - - await write_both( - {f"auth/{channel_id}/{user_id}/handler": new_flow_state_alpha.model_copy()} - ) - await write_both( - {f"auth/{channel_id}/{user_id}/auth_handler": flow_state_beta.model_copy()} - ) - await write_both({"other_data": MockStoreItem({"value": "more"})}) - - await delete_both(["some_data"]) - - await read_check([f"auth/{channel_id}/{user_id}/handler"], target_cls=FlowState) - await read_check( - [f"auth/{channel_id}/{user_id}/auth_handler"], target_cls=FlowState - ) - await read_check([f"auth/{channel_id}/{user_id}/chi"], target_cls=FlowState) - await read_check(["other_data"], target_cls=MockStoreItem) - await read_check(["some_data"], target_cls=MockStoreItem) +import pytest + +from microsoft_agents.hosting.core.storage import MemoryStorage +from microsoft_agents.hosting.core.oauth import FlowState, FlowStorageClient + +from tests._common.storage.utils import MockStoreItem + + +class TestFlowStorageClient: + + @pytest.fixture + def channel_id(self): + return "__channel_id" + + @pytest.fixture + def user_id(self): + return "__user_id" + + @pytest.fixture + def storage(self): + return MemoryStorage() + + @pytest.fixture + def client(self, channel_id, user_id, storage): + return FlowStorageClient(channel_id, user_id, storage) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "channel_id, user_id", + [ + ("channel_id", "user_id"), + ("teams_id", "Bob"), + ("channel", "Alice"), + ], + ) + async def test_init_base_key(self, mocker, channel_id, user_id): + client = FlowStorageClient(channel_id, user_id, mocker.Mock()) + assert client.base_key == f"auth/{channel_id}/{user_id}/" + + @pytest.mark.asyncio + async def test_init_fails_without_user_id(self, channel_id, storage): + with pytest.raises(ValueError): + FlowStorageClient(channel_id, "", storage) + + @pytest.mark.asyncio + async def test_init_fails_without_channel_id(self, user_id, storage): + with pytest.raises(ValueError): + FlowStorageClient("", user_id, storage) + + @pytest.mark.parametrize( + "auth_handler_id, expected", + [ + ("handler", "auth/__channel_id/__user_id/handler"), + ("auth_handler", "auth/__channel_id/__user_id/auth_handler"), + ], + ) + def test_key(self, client, auth_handler_id, expected): + assert client.key(auth_handler_id) == expected + + @pytest.mark.asyncio + @pytest.mark.parametrize("auth_handler_id", ["handler", "auth_handler"]) + async def test_read(self, mocker, user_id, channel_id, auth_handler_id): + storage = mocker.AsyncMock() + key = f"auth/{channel_id}/{user_id}/{auth_handler_id}" + storage.read.return_value = {key: FlowState()} + client = FlowStorageClient(channel_id, user_id, storage) + res = await client.read(auth_handler_id) + assert res is storage.read.return_value[key] + storage.read.assert_called_once_with( + [client.key(auth_handler_id)], target_cls=FlowState + ) + + @pytest.mark.asyncio + async def test_read_missing(self, mocker): + storage = mocker.AsyncMock() + storage.read.return_value = {} + client = FlowStorageClient("__channel_id", "__user_id", storage) + res = await client.read("non_existent_handler") + assert res is None + storage.read.assert_called_once_with( + [client.key("non_existent_handler")], target_cls=FlowState + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize("auth_handler_id", ["handler", "auth_handler"]) + async def test_write(self, mocker, channel_id, user_id, auth_handler_id): + storage = mocker.AsyncMock() + storage.write.return_value = None + client = FlowStorageClient(channel_id, user_id, storage) + flow_state = mocker.Mock(spec=FlowState) + flow_state.auth_handler_id = auth_handler_id + await client.write(flow_state) + storage.write.assert_called_once_with({client.key(auth_handler_id): flow_state}) + + @pytest.mark.asyncio + @pytest.mark.parametrize("auth_handler_id", ["handler", "auth_handler"]) + async def test_delete(self, mocker, channel_id, user_id, auth_handler_id): + storage = mocker.AsyncMock() + storage.delete.return_value = None + client = FlowStorageClient(channel_id, user_id, storage) + await client.delete(auth_handler_id) + storage.delete.assert_called_once_with([client.key(auth_handler_id)]) + + @pytest.mark.asyncio + async def test_integration_with_memory_storage(self, channel_id, user_id): + + flow_state_alpha = FlowState(auth_handler_id="handler", flow_started=True) + flow_state_beta = FlowState( + auth_handler_id="auth_handler", flow_started=True, user_token="token" + ) + + storage = MemoryStorage( + { + "some_data": MockStoreItem({"value": "test"}), + f"auth/{channel_id}/{user_id}/handler": flow_state_alpha, + f"auth/{channel_id}/{user_id}/auth_handler": flow_state_beta, + } + ) + baseline = MemoryStorage( + { + "some_data": MockStoreItem({"value": "test"}), + f"auth/{channel_id}/{user_id}/handler": flow_state_alpha, + f"fauth/{channel_id}/{user_id}/auth_handler": flow_state_beta, + } + ) + + # helpers + async def read_check(*args, **kwargs): + res_storage = await storage.read(*args, **kwargs) + res_baseline = await baseline.read(*args, **kwargs) + assert res_storage == res_baseline + + async def write_both(*args, **kwargs): + await storage.write(*args, **kwargs) + await baseline.write(*args, **kwargs) + + async def delete_both(*args, **kwargs): + await storage.delete(*args, **kwargs) + await baseline.delete(*args, **kwargs) + + client = FlowStorageClient(channel_id, user_id, storage) + + new_flow_state_alpha = FlowState(auth_handler_id="handler") + flow_state_chi = FlowState(auth_handler_id="chi") + + await client.write(new_flow_state_alpha) + await client.write(flow_state_chi) + await baseline.write( + {f"auth/{channel_id}/{user_id}/handler": new_flow_state_alpha.model_copy()} + ) + await baseline.write( + {f"auth/{channel_id}/{user_id}/chi": flow_state_chi.model_copy()} + ) + + await write_both( + {f"auth/{channel_id}/{user_id}/handler": new_flow_state_alpha.model_copy()} + ) + await write_both( + {f"auth/{channel_id}/{user_id}/auth_handler": flow_state_beta.model_copy()} + ) + await write_both({"other_data": MockStoreItem({"value": "more"})}) + + await delete_both(["some_data"]) + + await read_check([f"auth/{channel_id}/{user_id}/handler"], target_cls=FlowState) + await read_check( + [f"auth/{channel_id}/{user_id}/auth_handler"], target_cls=FlowState + ) + await read_check([f"auth/{channel_id}/{user_id}/chi"], target_cls=FlowState) + await read_check(["other_data"], target_cls=MockStoreItem) + await read_check(["some_data"], target_cls=MockStoreItem) diff --git a/libraries/microsoft-agents-hosting-core/tests/test_memory_storage.py b/tests/hosting_core/test_memory_storage.py similarity index 79% rename from libraries/microsoft-agents-hosting-core/tests/test_memory_storage.py rename to tests/hosting_core/test_memory_storage.py index 6e6cbfcd..e60f364e 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_memory_storage.py +++ b/tests/hosting_core/test_memory_storage.py @@ -1,5 +1,5 @@ from microsoft_agents.hosting.core.storage.memory_storage import MemoryStorage -from microsoft_agents.hosting.core.storage._storage_test_utils import CRUDStorageTests +from tests._common.storage.utils import CRUDStorageTests class TestMemoryStorage(CRUDStorageTests): diff --git a/libraries/microsoft-agents-hosting-core/tests/test_oauth_flow.py b/tests/hosting_core/test_oauth_flow.py similarity index 97% rename from libraries/microsoft-agents-hosting-core/tests/test_oauth_flow.py rename to tests/hosting_core/test_oauth_flow.py index 9a3b3dd1..ea6268b8 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_oauth_flow.py +++ b/tests/hosting_core/test_oauth_flow.py @@ -1,604 +1,604 @@ -import pytest - -from microsoft_agents.activity import ( - Activity, - ActivityTypes, - TokenResponse, - SignInResource, - TokenExchangeState, - ConversationReference, - ChannelAccount, -) -from microsoft_agents.hosting.core.oauth import ( - OAuthFlow, - FlowErrorTag, - FlowStateTag, - FlowResponse, -) -from microsoft_agents.hosting.core.connector.user_token_base import UserTokenBase -from microsoft_agents.hosting.core.connector.user_token_client_base import ( - UserTokenClientBase, -) - -# test constants -from core_tools.testing_oauth import * - - -class TestOAuthFlowUtils: - - def create_user_token_client(self, mocker, get_token_return=None): - - user_token_client = mocker.Mock(spec=UserTokenClientBase) - user_token_client.user_token = mocker.Mock(spec=UserTokenBase) - user_token_client.user_token.get_token = mocker.AsyncMock() - user_token_client.user_token.sign_out = mocker.AsyncMock() - - return_value = TokenResponse() - if get_token_return: - return_value = TokenResponse(token=get_token_return) - user_token_client.user_token.get_token.return_value = return_value - - return user_token_client - - @pytest.fixture - def user_token_client(self, mocker): - return self.create_user_token_client(mocker, get_token_return=RES_TOKEN) - - def create_activity( - self, - mocker, - activity_type=ActivityTypes.message, - name="a", - value=None, - text="a", - ): - # def conv_ref(): - # return mocker.MagicMock(spec=ConversationReference) - mock_conversation_ref = mocker.MagicMock(ConversationReference) - mocker.patch.object( - Activity, - "get_conversation_reference", - return_value=mocker.MagicMock(ConversationReference), - ) - # mocker.patch.object(ConversationReference, "create", return_value=conv_ref()) - return Activity( - type=activity_type, - name=name, - from_property=ChannelAccount(id=USER_ID), - channel_id=CHANNEL_ID, - # get_conversation_reference=mocker.Mock(return_value=conv_ref), - relates_to=mocker.MagicMock(ConversationReference), - value=value, - text=text, - ) - - @pytest.fixture(params=FLOW_STATES.ALL()) - def sample_flow_state(self, request): - return request.param.model_copy() - - @pytest.fixture(params=FLOW_STATES.FAILED()) - def sample_failed_flow_state(self, request): - return request.param.model_copy() - - @pytest.fixture(params=FLOW_STATES.INACTIVE()) - def sample_inactive_flow_state(self, request): - return request.param.model_copy() - - @pytest.fixture( - params=[ - flow_state - for flow_state in FLOW_STATES.INACTIVE() - if flow_state.tag != FlowStateTag.COMPLETE - ] - ) - def sample_inactive_flow_state_not_completed(self, request): - return request.param.model_copy() - - @pytest.fixture(params=FLOW_STATES.ACTIVE()) - def sample_active_flow_state(self, request): - return request.param.model_copy() - - @pytest.fixture - def flow(self, sample_flow_state, user_token_client): - return OAuthFlow(sample_flow_state, user_token_client) - - -class TestOAuthFlow(TestOAuthFlowUtils): - - def test_init_no_user_token_client(self, sample_flow_state): - with pytest.raises(ValueError): - OAuthFlow(sample_flow_state, None) - - @pytest.mark.parametrize( - "missing_value", ["connection", "ms_app_id", "channel_id", "user_id"] - ) - def test_init_errors(self, missing_value, user_token_client): - flow_state = FLOW_STATES.STARTED_FLOW.model_copy() - flow_state.__setattr__(missing_value, None) - with pytest.raises(ValueError): - OAuthFlow(flow_state, user_token_client) - flow_state.__setattr__(missing_value, "") - with pytest.raises(ValueError): - OAuthFlow(flow_state, user_token_client) - - def test_init_with_state(self, sample_flow_state, user_token_client): - flow = OAuthFlow(sample_flow_state, user_token_client) - assert flow.flow_state == sample_flow_state - - def test_flow_state_prop_copy(self, flow): - flow_state = flow.flow_state - flow_state.user_id = flow_state.user_id + "_copy" - assert flow.flow_state.user_id == USER_ID - assert flow_state.user_id == f"{USER_ID}_copy" - - @pytest.mark.asyncio - async def test_get_user_token_success(self, sample_flow_state, user_token_client): - # setup - flow = OAuthFlow(sample_flow_state, user_token_client) - expected_final_flow_state = sample_flow_state - expected_final_flow_state.user_token = RES_TOKEN - expected_final_flow_state.tag = FlowStateTag.COMPLETE - - # test - token_response = await flow.get_user_token() - token = token_response.token - - # verify - assert token == RES_TOKEN - expected_final_flow_state.expiration = flow.flow_state.expiration - assert flow.flow_state == expected_final_flow_state - user_token_client.user_token.get_token.assert_called_once_with( - user_id=USER_ID, - connection_name=ABS_OAUTH_CONNECTION_NAME, - channel_id=CHANNEL_ID, - code=None, - ) - - @pytest.mark.asyncio - async def test_get_user_token_failure(self, mocker, sample_flow_state): - # setup - user_token_client = self.create_user_token_client(mocker, get_token_return=None) - flow = OAuthFlow(sample_flow_state, user_token_client) - expected_final_flow_state = flow.flow_state - - # test - token_response = await flow.get_user_token() - - # verify - assert token_response == TokenResponse() - assert flow.flow_state == expected_final_flow_state - user_token_client.user_token.get_token.assert_called_once_with( - user_id=USER_ID, - connection_name=ABS_OAUTH_CONNECTION_NAME, - channel_id=CHANNEL_ID, - code=None, - ) - - @pytest.mark.asyncio - async def test_sign_out(self, sample_flow_state, user_token_client): - # setup - flow = OAuthFlow(sample_flow_state, user_token_client) - expected_flow_state = sample_flow_state - expected_flow_state.user_token = "" - expected_flow_state.tag = FlowStateTag.NOT_STARTED - - # test - await flow.sign_out() - - # verify - user_token_client.user_token.sign_out.assert_called_once_with( - user_id=USER_ID, - connection_name=ABS_OAUTH_CONNECTION_NAME, - channel_id=CHANNEL_ID, - ) - assert flow.flow_state == expected_flow_state - - @pytest.mark.asyncio - async def test_begin_flow_easy_case( - self, mocker, sample_flow_state, user_token_client - ): - # setup - flow = OAuthFlow(sample_flow_state, user_token_client) - activity = mocker.Mock(spec=Activity) - expected_flow_state = sample_flow_state - expected_flow_state.user_token = RES_TOKEN - expected_flow_state.tag = FlowStateTag.COMPLETE - - # test - response = await flow.begin_flow(activity) - - # verify - flow_state = flow.flow_state - expected_flow_state.expiration = flow_state.expiration - assert flow_state == expected_flow_state - - assert response.flow_state == flow_state - assert response.sign_in_resource is None # No sign-in resource in this case - assert response.flow_error_tag == FlowErrorTag.NONE - assert response.token_response.token == RES_TOKEN - user_token_client.user_token.get_token.assert_called_once_with( - user_id=USER_ID, - connection_name=ABS_OAUTH_CONNECTION_NAME, - channel_id=CHANNEL_ID, - code=None, - ) - - @pytest.mark.asyncio - async def test_begin_flow_long_case( - self, mocker, sample_flow_state, user_token_client - ): - # mock - # tes = mocker.Mock(TokenExchangeState) - # tes.get_encoded_state = mocker.Mock(return_value="encoded_state") - mocker.patch.object( - TokenExchangeState, "get_encoded_state", return_value="encoded_state" - ) - dummy_sign_in_resource = SignInResource( - sign_in_link="https://example.com/signin", - token_exchange_state=mocker.Mock( - TokenExchangeState, - get_encoded_state=mocker.Mock(return_value="encoded_state"), - ), - ) - user_token_client.user_token.get_token = mocker.AsyncMock( - return_value=TokenResponse() - ) - user_token_client.agent_sign_in.get_sign_in_resource = mocker.AsyncMock( - return_value=dummy_sign_in_resource - ) - activity = self.create_activity(mocker) - - # setup - flow = OAuthFlow(sample_flow_state, user_token_client) - expected_flow_state = sample_flow_state - expected_flow_state.user_token = "" - expected_flow_state.tag = FlowStateTag.BEGIN - expected_flow_state.attempts_remaining = 3 - expected_flow_state.continuation_activity = activity - - # test - response = await flow.begin_flow(activity) - - # verify flow_state - flow_state = flow.flow_state - expected_flow_state.expiration = ( - flow_state.expiration - ) # robrandao: TODO -> ignore this for now - assert flow_state == response.flow_state - assert flow_state == expected_flow_state - - # verify FlowResponse - assert response.sign_in_resource == dummy_sign_in_resource - assert response.flow_error_tag == FlowErrorTag.NONE - assert not response.token_response - # robrandao: TODO more assertions on sign_in_resource - - @pytest.mark.asyncio - async def test_continue_flow_not_active( - self, mocker, sample_inactive_flow_state, user_token_client - ): - # setup - activity = mocker.Mock() - flow = OAuthFlow(sample_inactive_flow_state, user_token_client) - expected_flow_state = sample_inactive_flow_state - expected_flow_state.tag = FlowStateTag.FAILURE - - # test - flow_response = await flow.continue_flow(activity) - flow_state = flow.flow_state - - # verify - assert flow_state == expected_flow_state - assert flow_response.flow_state == flow_state - assert not flow_response.token_response - - async def helper_continue_flow_failure( - self, active_flow_state, user_token_client, activity, flow_error_tag - ): - # setup - flow = OAuthFlow(active_flow_state, user_token_client) - expected_flow_state = active_flow_state - expected_flow_state.tag = ( - FlowStateTag.CONTINUE - if active_flow_state.attempts_remaining > 1 - else FlowStateTag.FAILURE - ) - expected_flow_state.attempts_remaining = ( - active_flow_state.attempts_remaining - 1 - ) - - # test - flow_response = await flow.continue_flow(activity) - flow_state = flow.flow_state - - # verify - assert flow_response.flow_state == flow_state - assert expected_flow_state == flow_state - assert flow_response.token_response == TokenResponse() - assert flow_response.flow_error_tag == flow_error_tag - - async def helper_continue_flow_success( - self, active_flow_state, user_token_client, activity - ): - # setup - flow = OAuthFlow(active_flow_state, user_token_client) - expected_flow_state = active_flow_state - expected_flow_state.tag = FlowStateTag.COMPLETE - expected_flow_state.user_token = RES_TOKEN - expected_flow_state.attempts_remaining = active_flow_state.attempts_remaining - - # test - flow_response = await flow.continue_flow(activity) - flow_state = flow.flow_state - expected_flow_state.expiration = ( - flow_state.expiration - ) # robrandao: TODO -> ignore this for now - - # verify - assert flow_response.flow_state == flow_state - assert expected_flow_state == flow_state - assert flow_response.token_response == TokenResponse(token=RES_TOKEN) - assert flow_response.flow_error_tag == FlowErrorTag.NONE - - @pytest.mark.asyncio - @pytest.mark.parametrize("magic_code", ["magic", "123", "", "1239453"]) - async def test_continue_flow_active_message_magic_format_error( - self, mocker, sample_active_flow_state, user_token_client, magic_code - ): - # setup - activity = self.create_activity(mocker, ActivityTypes.message, text=magic_code) - await self.helper_continue_flow_failure( - sample_active_flow_state, - user_token_client, - activity, - FlowErrorTag.MAGIC_FORMAT, - ) - user_token_client.assert_not_called() - - @pytest.mark.asyncio - async def test_continue_flow_active_message_magic_code_error( - self, mocker, sample_active_flow_state, user_token_client - ): - # setup - user_token_client.user_token.get_token = mocker.AsyncMock( - return_value=TokenResponse() - ) - activity = self.create_activity(mocker, ActivityTypes.message, text="123456") - await self.helper_continue_flow_failure( - sample_active_flow_state, - user_token_client, - activity, - FlowErrorTag.MAGIC_CODE_INCORRECT, - ) - user_token_client.user_token.get_token.assert_called_once_with( - user_id=sample_active_flow_state.user_id, - connection_name=sample_active_flow_state.connection, - channel_id=sample_active_flow_state.channel_id, - code="123456", - ) - - @pytest.mark.asyncio - async def test_continue_flow_active_message_success( - self, mocker, sample_active_flow_state, user_token_client - ): - # setup - activity = self.create_activity(mocker, ActivityTypes.message, text="123456") - await self.helper_continue_flow_success( - sample_active_flow_state, user_token_client, activity - ) - user_token_client.user_token.get_token.assert_called_once_with( - user_id=sample_active_flow_state.user_id, - connection_name=sample_active_flow_state.connection, - channel_id=sample_active_flow_state.channel_id, - code="123456", - ) - - @pytest.mark.asyncio - async def test_continue_flow_active_sign_in_verify_state_error( - self, mocker, sample_active_flow_state, user_token_client - ): - # setup - user_token_client.user_token.get_token = mocker.AsyncMock( - return_value=TokenResponse() - ) - activity = self.create_activity( - mocker, - ActivityTypes.invoke, - name="signin/verifyState", - value={"state": "magic_code"}, - ) - await self.helper_continue_flow_failure( - sample_active_flow_state, user_token_client, activity, FlowErrorTag.OTHER - ) - user_token_client.user_token.get_token.assert_called_once_with( - user_id=sample_active_flow_state.user_id, - connection_name=sample_active_flow_state.connection, - channel_id=sample_active_flow_state.channel_id, - code="magic_code", - ) - - @pytest.mark.asyncio - async def test_continue_flow_active_sign_in_verify_success( - self, mocker, sample_active_flow_state, user_token_client - ): - activity = self.create_activity( - mocker, - ActivityTypes.invoke, - name="signin/verifyState", - value={"state": "magic_code"}, - ) - await self.helper_continue_flow_success( - sample_active_flow_state, user_token_client, activity - ) - user_token_client.user_token.get_token.assert_called_once_with( - user_id=sample_active_flow_state.user_id, - connection_name=sample_active_flow_state.connection, - channel_id=sample_active_flow_state.channel_id, - code="magic_code", - ) - - @pytest.mark.asyncio - async def test_continue_flow_active_sign_in_token_exchange_error( - self, mocker, sample_active_flow_state, user_token_client - ): - token_exchange_request = {} - user_token_client.user_token.exchange_token = mocker.AsyncMock( - return_value=TokenResponse() - ) - activity = self.create_activity( - mocker, - ActivityTypes.invoke, - name="signin/tokenExchange", - value=token_exchange_request, - ) - await self.helper_continue_flow_failure( - sample_active_flow_state, user_token_client, activity, FlowErrorTag.OTHER - ) - user_token_client.user_token.exchange_token.assert_called_once_with( - user_id=sample_active_flow_state.user_id, - connection_name=sample_active_flow_state.connection, - channel_id=sample_active_flow_state.channel_id, - body=token_exchange_request, - ) - - @pytest.mark.asyncio - async def test_continue_flow_active_sign_in_token_exchange_success( - self, mocker, sample_active_flow_state, user_token_client - ): - token_exchange_request = {} - user_token_client.user_token.exchange_token = mocker.AsyncMock( - return_value=TokenResponse(token=RES_TOKEN) - ) - activity = self.create_activity( - mocker, - ActivityTypes.invoke, - name="signin/tokenExchange", - value=token_exchange_request, - ) - await self.helper_continue_flow_success( - sample_active_flow_state, user_token_client, activity - ) - user_token_client.user_token.exchange_token.assert_called_once_with( - user_id=sample_active_flow_state.user_id, - connection_name=sample_active_flow_state.connection, - channel_id=sample_active_flow_state.channel_id, - body=token_exchange_request, - ) - - @pytest.mark.asyncio - async def test_continue_flow_invalid_invoke_name( - self, mocker, sample_active_flow_state, user_token_client - ): - with pytest.raises(ValueError): - activity = self.create_activity( - mocker, ActivityTypes.invoke, name="other", value={} - ) - flow = OAuthFlow(sample_active_flow_state, user_token_client) - await flow.continue_flow(activity) - - @pytest.mark.asyncio - async def test_continue_flow_invalid_activity_type( - self, mocker, sample_active_flow_state, user_token_client - ): - with pytest.raises(ValueError): - activity = self.create_activity( - mocker, ActivityTypes.command, name="other", value={} - ) - flow = OAuthFlow(sample_active_flow_state, user_token_client) - await flow.continue_flow(activity) - - @pytest.mark.asyncio - async def test_begin_or_continue_flow_not_started_flow(self, mocker): - # setup - sample_flow_state = FLOW_STATES.NOT_STARTED_FLOW.model_copy() - expected_response = FlowResponse( - flow_state=sample_flow_state, - token_response=TokenResponse(token=sample_flow_state.user_token), - ) - mocker.patch.object(OAuthFlow, "begin_flow", return_value=expected_response) - - activity_mock = mocker.Mock() - flow = OAuthFlow(sample_flow_state, mocker.Mock()) - - # test - actual_response = await flow.begin_or_continue_flow(activity_mock) - - # verify - assert actual_response is expected_response - OAuthFlow.begin_flow.assert_called_once_with(activity_mock) - - @pytest.mark.asyncio - async def test_begin_or_continue_flow_inactive_flow( - self, - mocker, - sample_inactive_flow_state_not_completed, - ): - # setup - expected_response = FlowResponse( - flow_state=sample_inactive_flow_state_not_completed, - token_response=TokenResponse(), - ) - mocker.patch.object(OAuthFlow, "begin_flow", return_value=expected_response) - - flow = OAuthFlow(sample_inactive_flow_state_not_completed, mocker.Mock()) - - # test - activity_mock = mocker.Mock() - actual_response = await flow.begin_or_continue_flow(activity_mock) - - # verify - assert actual_response is expected_response - OAuthFlow.begin_flow.assert_called_once_with(activity_mock) - - @pytest.mark.asyncio - async def test_begin_or_continue_flow_active_flow( - self, - mocker, - sample_active_flow_state, - ): - # setup - expected_response = FlowResponse( - flow_state=sample_active_flow_state, - token_response=TokenResponse(token=sample_active_flow_state.user_token), - ) - mocker.patch.object(OAuthFlow, "continue_flow", return_value=expected_response) - - flow = OAuthFlow(sample_active_flow_state, mocker.Mock()) - - # test - activity_mock = mocker.Mock() - actual_response = await flow.begin_or_continue_flow(activity_mock) - - # verify - assert actual_response is expected_response - OAuthFlow.continue_flow.assert_called_once_with(activity_mock) - - @pytest.mark.asyncio - async def test_begin_or_continue_flow_stale_flow_state(self, mocker): - flow_state = FLOW_STATES.ACTIVE_EXP_FLOW.model_copy() - expected_response = FlowResponse() - - mocker.patch.object(OAuthFlow, "begin_flow", return_value=expected_response) - - flow = OAuthFlow(flow_state, mocker.Mock()) - actual_response = await flow.begin_or_continue_flow(None) - - assert actual_response is expected_response - OAuthFlow.begin_flow.assert_called_once_with(None) - - @pytest.mark.asyncio - async def test_begin_or_continue_flow_completed_flow_state(self, mocker): - flow_state = FLOW_STATES.COMPLETED_FLOW.model_copy() - expected_response = FlowResponse( - flow_state=flow_state, - token_response=TokenResponse(token=flow_state.user_token), - ) - mocker.patch.object(OAuthFlow, "begin_flow", return_value=None) - mocker.patch.object(OAuthFlow, "continue_flow", return_value=None) - - flow = OAuthFlow(flow_state, mocker.Mock()) - actual_response = await flow.begin_or_continue_flow(None) - - assert actual_response == expected_response - OAuthFlow.begin_flow.assert_not_called() - OAuthFlow.continue_flow.assert_not_called() +import pytest + +from microsoft_agents.activity import ( + Activity, + ActivityTypes, + TokenResponse, + SignInResource, + TokenExchangeState, + ConversationReference, + ChannelAccount, +) +from microsoft_agents.hosting.core.oauth import ( + OAuthFlow, + FlowErrorTag, + FlowStateTag, + FlowResponse, +) +from microsoft_agents.hosting.core.connector.user_token_base import UserTokenBase +from microsoft_agents.hosting.core.connector.user_token_client_base import ( + UserTokenClientBase, +) + +# test constants +from .tools.testing_oauth import * + + +class TestOAuthFlowUtils: + + def create_user_token_client(self, mocker, get_token_return=None): + + user_token_client = mocker.Mock(spec=UserTokenClientBase) + user_token_client.user_token = mocker.Mock(spec=UserTokenBase) + user_token_client.user_token.get_token = mocker.AsyncMock() + user_token_client.user_token.sign_out = mocker.AsyncMock() + + return_value = TokenResponse() + if get_token_return: + return_value = TokenResponse(token=get_token_return) + user_token_client.user_token.get_token.return_value = return_value + + return user_token_client + + @pytest.fixture + def user_token_client(self, mocker): + return self.create_user_token_client(mocker, get_token_return=RES_TOKEN) + + def create_activity( + self, + mocker, + activity_type=ActivityTypes.message, + name="a", + value=None, + text="a", + ): + # def conv_ref(): + # return mocker.MagicMock(spec=ConversationReference) + mock_conversation_ref = mocker.MagicMock(ConversationReference) + mocker.patch.object( + Activity, + "get_conversation_reference", + return_value=mocker.MagicMock(ConversationReference), + ) + # mocker.patch.object(ConversationReference, "create", return_value=conv_ref()) + return Activity( + type=activity_type, + name=name, + from_property=ChannelAccount(id=USER_ID), + channel_id=CHANNEL_ID, + # get_conversation_reference=mocker.Mock(return_value=conv_ref), + relates_to=mocker.MagicMock(ConversationReference), + value=value, + text=text, + ) + + @pytest.fixture(params=FLOW_STATES.ALL()) + def sample_flow_state(self, request): + return request.param.model_copy() + + @pytest.fixture(params=FLOW_STATES.FAILED()) + def sample_failed_flow_state(self, request): + return request.param.model_copy() + + @pytest.fixture(params=FLOW_STATES.INACTIVE()) + def sample_inactive_flow_state(self, request): + return request.param.model_copy() + + @pytest.fixture( + params=[ + flow_state + for flow_state in FLOW_STATES.INACTIVE() + if flow_state.tag != FlowStateTag.COMPLETE + ] + ) + def sample_inactive_flow_state_not_completed(self, request): + return request.param.model_copy() + + @pytest.fixture(params=FLOW_STATES.ACTIVE()) + def sample_active_flow_state(self, request): + return request.param.model_copy() + + @pytest.fixture + def flow(self, sample_flow_state, user_token_client): + return OAuthFlow(sample_flow_state, user_token_client) + + +class TestOAuthFlow(TestOAuthFlowUtils): + + def test_init_no_user_token_client(self, sample_flow_state): + with pytest.raises(ValueError): + OAuthFlow(sample_flow_state, None) + + @pytest.mark.parametrize( + "missing_value", ["connection", "ms_app_id", "channel_id", "user_id"] + ) + def test_init_errors(self, missing_value, user_token_client): + flow_state = FLOW_STATES.STARTED_FLOW.model_copy() + flow_state.__setattr__(missing_value, None) + with pytest.raises(ValueError): + OAuthFlow(flow_state, user_token_client) + flow_state.__setattr__(missing_value, "") + with pytest.raises(ValueError): + OAuthFlow(flow_state, user_token_client) + + def test_init_with_state(self, sample_flow_state, user_token_client): + flow = OAuthFlow(sample_flow_state, user_token_client) + assert flow.flow_state == sample_flow_state + + def test_flow_state_prop_copy(self, flow): + flow_state = flow.flow_state + flow_state.user_id = flow_state.user_id + "_copy" + assert flow.flow_state.user_id == USER_ID + assert flow_state.user_id == f"{USER_ID}_copy" + + @pytest.mark.asyncio + async def test_get_user_token_success(self, sample_flow_state, user_token_client): + # setup + flow = OAuthFlow(sample_flow_state, user_token_client) + expected_final_flow_state = sample_flow_state + expected_final_flow_state.user_token = RES_TOKEN + expected_final_flow_state.tag = FlowStateTag.COMPLETE + + # test + token_response = await flow.get_user_token() + token = token_response.token + + # verify + assert token == RES_TOKEN + expected_final_flow_state.expiration = flow.flow_state.expiration + assert flow.flow_state == expected_final_flow_state + user_token_client.user_token.get_token.assert_called_once_with( + user_id=USER_ID, + connection_name=ABS_OAUTH_CONNECTION_NAME, + channel_id=CHANNEL_ID, + code=None, + ) + + @pytest.mark.asyncio + async def test_get_user_token_failure(self, mocker, sample_flow_state): + # setup + user_token_client = self.create_user_token_client(mocker, get_token_return=None) + flow = OAuthFlow(sample_flow_state, user_token_client) + expected_final_flow_state = flow.flow_state + + # test + token_response = await flow.get_user_token() + + # verify + assert token_response == TokenResponse() + assert flow.flow_state == expected_final_flow_state + user_token_client.user_token.get_token.assert_called_once_with( + user_id=USER_ID, + connection_name=ABS_OAUTH_CONNECTION_NAME, + channel_id=CHANNEL_ID, + code=None, + ) + + @pytest.mark.asyncio + async def test_sign_out(self, sample_flow_state, user_token_client): + # setup + flow = OAuthFlow(sample_flow_state, user_token_client) + expected_flow_state = sample_flow_state + expected_flow_state.user_token = "" + expected_flow_state.tag = FlowStateTag.NOT_STARTED + + # test + await flow.sign_out() + + # verify + user_token_client.user_token.sign_out.assert_called_once_with( + user_id=USER_ID, + connection_name=ABS_OAUTH_CONNECTION_NAME, + channel_id=CHANNEL_ID, + ) + assert flow.flow_state == expected_flow_state + + @pytest.mark.asyncio + async def test_begin_flow_easy_case( + self, mocker, sample_flow_state, user_token_client + ): + # setup + flow = OAuthFlow(sample_flow_state, user_token_client) + activity = mocker.Mock(spec=Activity) + expected_flow_state = sample_flow_state + expected_flow_state.user_token = RES_TOKEN + expected_flow_state.tag = FlowStateTag.COMPLETE + + # test + response = await flow.begin_flow(activity) + + # verify + flow_state = flow.flow_state + expected_flow_state.expiration = flow_state.expiration + assert flow_state == expected_flow_state + + assert response.flow_state == flow_state + assert response.sign_in_resource is None # No sign-in resource in this case + assert response.flow_error_tag == FlowErrorTag.NONE + assert response.token_response.token == RES_TOKEN + user_token_client.user_token.get_token.assert_called_once_with( + user_id=USER_ID, + connection_name=ABS_OAUTH_CONNECTION_NAME, + channel_id=CHANNEL_ID, + code=None, + ) + + @pytest.mark.asyncio + async def test_begin_flow_long_case( + self, mocker, sample_flow_state, user_token_client + ): + # mock + # tes = mocker.Mock(TokenExchangeState) + # tes.get_encoded_state = mocker.Mock(return_value="encoded_state") + mocker.patch.object( + TokenExchangeState, "get_encoded_state", return_value="encoded_state" + ) + dummy_sign_in_resource = SignInResource( + sign_in_link="https://example.com/signin", + token_exchange_state=mocker.Mock( + TokenExchangeState, + get_encoded_state=mocker.Mock(return_value="encoded_state"), + ), + ) + user_token_client.user_token.get_token = mocker.AsyncMock( + return_value=TokenResponse() + ) + user_token_client.agent_sign_in.get_sign_in_resource = mocker.AsyncMock( + return_value=dummy_sign_in_resource + ) + activity = self.create_activity(mocker) + + # setup + flow = OAuthFlow(sample_flow_state, user_token_client) + expected_flow_state = sample_flow_state + expected_flow_state.user_token = "" + expected_flow_state.tag = FlowStateTag.BEGIN + expected_flow_state.attempts_remaining = 3 + expected_flow_state.continuation_activity = activity + + # test + response = await flow.begin_flow(activity) + + # verify flow_state + flow_state = flow.flow_state + expected_flow_state.expiration = ( + flow_state.expiration + ) # robrandao: TODO -> ignore this for now + assert flow_state == response.flow_state + assert flow_state == expected_flow_state + + # verify FlowResponse + assert response.sign_in_resource == dummy_sign_in_resource + assert response.flow_error_tag == FlowErrorTag.NONE + assert not response.token_response + # robrandao: TODO more assertions on sign_in_resource + + @pytest.mark.asyncio + async def test_continue_flow_not_active( + self, mocker, sample_inactive_flow_state, user_token_client + ): + # setup + activity = mocker.Mock() + flow = OAuthFlow(sample_inactive_flow_state, user_token_client) + expected_flow_state = sample_inactive_flow_state + expected_flow_state.tag = FlowStateTag.FAILURE + + # test + flow_response = await flow.continue_flow(activity) + flow_state = flow.flow_state + + # verify + assert flow_state == expected_flow_state + assert flow_response.flow_state == flow_state + assert not flow_response.token_response + + async def helper_continue_flow_failure( + self, active_flow_state, user_token_client, activity, flow_error_tag + ): + # setup + flow = OAuthFlow(active_flow_state, user_token_client) + expected_flow_state = active_flow_state + expected_flow_state.tag = ( + FlowStateTag.CONTINUE + if active_flow_state.attempts_remaining > 1 + else FlowStateTag.FAILURE + ) + expected_flow_state.attempts_remaining = ( + active_flow_state.attempts_remaining - 1 + ) + + # test + flow_response = await flow.continue_flow(activity) + flow_state = flow.flow_state + + # verify + assert flow_response.flow_state == flow_state + assert expected_flow_state == flow_state + assert flow_response.token_response == TokenResponse() + assert flow_response.flow_error_tag == flow_error_tag + + async def helper_continue_flow_success( + self, active_flow_state, user_token_client, activity + ): + # setup + flow = OAuthFlow(active_flow_state, user_token_client) + expected_flow_state = active_flow_state + expected_flow_state.tag = FlowStateTag.COMPLETE + expected_flow_state.user_token = RES_TOKEN + expected_flow_state.attempts_remaining = active_flow_state.attempts_remaining + + # test + flow_response = await flow.continue_flow(activity) + flow_state = flow.flow_state + expected_flow_state.expiration = ( + flow_state.expiration + ) # robrandao: TODO -> ignore this for now + + # verify + assert flow_response.flow_state == flow_state + assert expected_flow_state == flow_state + assert flow_response.token_response == TokenResponse(token=RES_TOKEN) + assert flow_response.flow_error_tag == FlowErrorTag.NONE + + @pytest.mark.asyncio + @pytest.mark.parametrize("magic_code", ["magic", "123", "", "1239453"]) + async def test_continue_flow_active_message_magic_format_error( + self, mocker, sample_active_flow_state, user_token_client, magic_code + ): + # setup + activity = self.create_activity(mocker, ActivityTypes.message, text=magic_code) + await self.helper_continue_flow_failure( + sample_active_flow_state, + user_token_client, + activity, + FlowErrorTag.MAGIC_FORMAT, + ) + user_token_client.assert_not_called() + + @pytest.mark.asyncio + async def test_continue_flow_active_message_magic_code_error( + self, mocker, sample_active_flow_state, user_token_client + ): + # setup + user_token_client.user_token.get_token = mocker.AsyncMock( + return_value=TokenResponse() + ) + activity = self.create_activity(mocker, ActivityTypes.message, text="123456") + await self.helper_continue_flow_failure( + sample_active_flow_state, + user_token_client, + activity, + FlowErrorTag.MAGIC_CODE_INCORRECT, + ) + user_token_client.user_token.get_token.assert_called_once_with( + user_id=sample_active_flow_state.user_id, + connection_name=sample_active_flow_state.connection, + channel_id=sample_active_flow_state.channel_id, + code="123456", + ) + + @pytest.mark.asyncio + async def test_continue_flow_active_message_success( + self, mocker, sample_active_flow_state, user_token_client + ): + # setup + activity = self.create_activity(mocker, ActivityTypes.message, text="123456") + await self.helper_continue_flow_success( + sample_active_flow_state, user_token_client, activity + ) + user_token_client.user_token.get_token.assert_called_once_with( + user_id=sample_active_flow_state.user_id, + connection_name=sample_active_flow_state.connection, + channel_id=sample_active_flow_state.channel_id, + code="123456", + ) + + @pytest.mark.asyncio + async def test_continue_flow_active_sign_in_verify_state_error( + self, mocker, sample_active_flow_state, user_token_client + ): + # setup + user_token_client.user_token.get_token = mocker.AsyncMock( + return_value=TokenResponse() + ) + activity = self.create_activity( + mocker, + ActivityTypes.invoke, + name="signin/verifyState", + value={"state": "magic_code"}, + ) + await self.helper_continue_flow_failure( + sample_active_flow_state, user_token_client, activity, FlowErrorTag.OTHER + ) + user_token_client.user_token.get_token.assert_called_once_with( + user_id=sample_active_flow_state.user_id, + connection_name=sample_active_flow_state.connection, + channel_id=sample_active_flow_state.channel_id, + code="magic_code", + ) + + @pytest.mark.asyncio + async def test_continue_flow_active_sign_in_verify_success( + self, mocker, sample_active_flow_state, user_token_client + ): + activity = self.create_activity( + mocker, + ActivityTypes.invoke, + name="signin/verifyState", + value={"state": "magic_code"}, + ) + await self.helper_continue_flow_success( + sample_active_flow_state, user_token_client, activity + ) + user_token_client.user_token.get_token.assert_called_once_with( + user_id=sample_active_flow_state.user_id, + connection_name=sample_active_flow_state.connection, + channel_id=sample_active_flow_state.channel_id, + code="magic_code", + ) + + @pytest.mark.asyncio + async def test_continue_flow_active_sign_in_token_exchange_error( + self, mocker, sample_active_flow_state, user_token_client + ): + token_exchange_request = {} + user_token_client.user_token.exchange_token = mocker.AsyncMock( + return_value=TokenResponse() + ) + activity = self.create_activity( + mocker, + ActivityTypes.invoke, + name="signin/tokenExchange", + value=token_exchange_request, + ) + await self.helper_continue_flow_failure( + sample_active_flow_state, user_token_client, activity, FlowErrorTag.OTHER + ) + user_token_client.user_token.exchange_token.assert_called_once_with( + user_id=sample_active_flow_state.user_id, + connection_name=sample_active_flow_state.connection, + channel_id=sample_active_flow_state.channel_id, + body=token_exchange_request, + ) + + @pytest.mark.asyncio + async def test_continue_flow_active_sign_in_token_exchange_success( + self, mocker, sample_active_flow_state, user_token_client + ): + token_exchange_request = {} + user_token_client.user_token.exchange_token = mocker.AsyncMock( + return_value=TokenResponse(token=RES_TOKEN) + ) + activity = self.create_activity( + mocker, + ActivityTypes.invoke, + name="signin/tokenExchange", + value=token_exchange_request, + ) + await self.helper_continue_flow_success( + sample_active_flow_state, user_token_client, activity + ) + user_token_client.user_token.exchange_token.assert_called_once_with( + user_id=sample_active_flow_state.user_id, + connection_name=sample_active_flow_state.connection, + channel_id=sample_active_flow_state.channel_id, + body=token_exchange_request, + ) + + @pytest.mark.asyncio + async def test_continue_flow_invalid_invoke_name( + self, mocker, sample_active_flow_state, user_token_client + ): + with pytest.raises(ValueError): + activity = self.create_activity( + mocker, ActivityTypes.invoke, name="other", value={} + ) + flow = OAuthFlow(sample_active_flow_state, user_token_client) + await flow.continue_flow(activity) + + @pytest.mark.asyncio + async def test_continue_flow_invalid_activity_type( + self, mocker, sample_active_flow_state, user_token_client + ): + with pytest.raises(ValueError): + activity = self.create_activity( + mocker, ActivityTypes.command, name="other", value={} + ) + flow = OAuthFlow(sample_active_flow_state, user_token_client) + await flow.continue_flow(activity) + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_not_started_flow(self, mocker): + # setup + sample_flow_state = FLOW_STATES.NOT_STARTED_FLOW.model_copy() + expected_response = FlowResponse( + flow_state=sample_flow_state, + token_response=TokenResponse(token=sample_flow_state.user_token), + ) + mocker.patch.object(OAuthFlow, "begin_flow", return_value=expected_response) + + activity_mock = mocker.Mock() + flow = OAuthFlow(sample_flow_state, mocker.Mock()) + + # test + actual_response = await flow.begin_or_continue_flow(activity_mock) + + # verify + assert actual_response is expected_response + OAuthFlow.begin_flow.assert_called_once_with(activity_mock) + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_inactive_flow( + self, + mocker, + sample_inactive_flow_state_not_completed, + ): + # setup + expected_response = FlowResponse( + flow_state=sample_inactive_flow_state_not_completed, + token_response=TokenResponse(), + ) + mocker.patch.object(OAuthFlow, "begin_flow", return_value=expected_response) + + flow = OAuthFlow(sample_inactive_flow_state_not_completed, mocker.Mock()) + + # test + activity_mock = mocker.Mock() + actual_response = await flow.begin_or_continue_flow(activity_mock) + + # verify + assert actual_response is expected_response + OAuthFlow.begin_flow.assert_called_once_with(activity_mock) + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_active_flow( + self, + mocker, + sample_active_flow_state, + ): + # setup + expected_response = FlowResponse( + flow_state=sample_active_flow_state, + token_response=TokenResponse(token=sample_active_flow_state.user_token), + ) + mocker.patch.object(OAuthFlow, "continue_flow", return_value=expected_response) + + flow = OAuthFlow(sample_active_flow_state, mocker.Mock()) + + # test + activity_mock = mocker.Mock() + actual_response = await flow.begin_or_continue_flow(activity_mock) + + # verify + assert actual_response is expected_response + OAuthFlow.continue_flow.assert_called_once_with(activity_mock) + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_stale_flow_state(self, mocker): + flow_state = FLOW_STATES.ACTIVE_EXP_FLOW.model_copy() + expected_response = FlowResponse() + + mocker.patch.object(OAuthFlow, "begin_flow", return_value=expected_response) + + flow = OAuthFlow(flow_state, mocker.Mock()) + actual_response = await flow.begin_or_continue_flow(None) + + assert actual_response is expected_response + OAuthFlow.begin_flow.assert_called_once_with(None) + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_completed_flow_state(self, mocker): + flow_state = FLOW_STATES.COMPLETED_FLOW.model_copy() + expected_response = FlowResponse( + flow_state=flow_state, + token_response=TokenResponse(token=flow_state.user_token), + ) + mocker.patch.object(OAuthFlow, "begin_flow", return_value=None) + mocker.patch.object(OAuthFlow, "continue_flow", return_value=None) + + flow = OAuthFlow(flow_state, mocker.Mock()) + actual_response = await flow.begin_or_continue_flow(None) + + assert actual_response == expected_response + OAuthFlow.begin_flow.assert_not_called() + OAuthFlow.continue_flow.assert_not_called() diff --git a/libraries/microsoft-agents-hosting-core/tests/test_state.py b/tests/hosting_core/test_state.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/test_state.py rename to tests/hosting_core/test_state.py diff --git a/libraries/microsoft-agents-hosting-core/tests/test_turn_context.py b/tests/hosting_core/test_turn_context.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/test_turn_context.py rename to tests/hosting_core/test_turn_context.py diff --git a/libraries/microsoft-agents-hosting-core/tests/test_utils.py b/tests/hosting_core/test_utils.py similarity index 97% rename from libraries/microsoft-agents-hosting-core/tests/test_utils.py rename to tests/hosting_core/test_utils.py index 98a408fc..e96a70ca 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_utils.py +++ b/tests/hosting_core/test_utils.py @@ -1,4 +1,4 @@ -from microsoft_agents.hosting.core.storage._storage_test_utils import ( +from tests._common.storage.utils import ( MockStoreItem, MockStoreItemB, my_deepcopy, diff --git a/tests/hosting_core/tools/__init__.py b/tests/hosting_core/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libraries/microsoft-agents-hosting-core/tests/core_tools/mock_user_token_client.py b/tests/hosting_core/tools/mock_user_token_client.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/core_tools/mock_user_token_client.py rename to tests/hosting_core/tools/mock_user_token_client.py diff --git a/libraries/microsoft-agents-hosting-core/tests/core_tools/testing_adapter.py b/tests/hosting_core/tools/testing_adapter.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/core_tools/testing_adapter.py rename to tests/hosting_core/tools/testing_adapter.py diff --git a/libraries/microsoft-agents-hosting-core/tests/core_tools/testing_authorization.py b/tests/hosting_core/tools/testing_authorization.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/core_tools/testing_authorization.py rename to tests/hosting_core/tools/testing_authorization.py diff --git a/libraries/microsoft-agents-hosting-core/tests/core_tools/testing_flow.py b/tests/hosting_core/tools/testing_flow.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/core_tools/testing_flow.py rename to tests/hosting_core/tools/testing_flow.py diff --git a/libraries/microsoft-agents-hosting-core/tests/core_tools/testing_oauth.py b/tests/hosting_core/tools/testing_oauth.py similarity index 98% rename from libraries/microsoft-agents-hosting-core/tests/core_tools/testing_oauth.py rename to tests/hosting_core/tools/testing_oauth.py index 28c6afa8..1128f1ce 100644 --- a/libraries/microsoft-agents-hosting-core/tests/core_tools/testing_oauth.py +++ b/tests/hosting_core/tools/testing_oauth.py @@ -1,8 +1,10 @@ from datetime import datetime -from microsoft_agents.hosting.core.storage._storage_test_utils import MockStoreItem from microsoft_agents.hosting.core.oauth.flow_state import FlowState, FlowStateTag +from tests._common.storage.utils import MockStoreItem + + MS_APP_ID = "__ms_app_id" CHANNEL_ID = "__channel_id" USER_ID = "__user_id" diff --git a/libraries/microsoft-agents-hosting-core/tests/core_tools/testing_utility.py b/tests/hosting_core/tools/testing_utility.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/tests/core_tools/testing_utility.py rename to tests/hosting_core/tools/testing_utility.py diff --git a/tests/hosting_teams/__init__.py b/tests/hosting_teams/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/storage_blob/__init__.py b/tests/storage_blob/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libraries/microsoft-agents-storage-blob/tests/test_blob_storage.py b/tests/storage_blob/test_blob_storage.py similarity index 99% rename from libraries/microsoft-agents-storage-blob/tests/test_blob_storage.py rename to tests/storage_blob/test_blob_storage.py index b3fde01c..a8a2825f 100644 --- a/libraries/microsoft-agents-storage-blob/tests/test_blob_storage.py +++ b/tests/storage_blob/test_blob_storage.py @@ -10,7 +10,7 @@ from azure.storage.blob.aio import BlobServiceClient from azure.core.exceptions import ResourceNotFoundError -from microsoft_agents.hosting.core.storage._storage_test_utils import ( +from tests._common.storage.utils import ( CRUDStorageTests, StorageBaseline, MockStoreItem, diff --git a/tests/storage_cosmos/__init__.py b/tests/storage_cosmos/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libraries/microsoft-agents-storage-cosmos/tests/test_cosmos_db_config.py b/tests/storage_cosmos/test_cosmos_db_config.py similarity index 100% rename from libraries/microsoft-agents-storage-cosmos/tests/test_cosmos_db_config.py rename to tests/storage_cosmos/test_cosmos_db_config.py diff --git a/libraries/microsoft-agents-storage-cosmos/tests/test_cosmos_db_storage.py b/tests/storage_cosmos/test_cosmos_db_storage.py similarity index 99% rename from libraries/microsoft-agents-storage-cosmos/tests/test_cosmos_db_storage.py rename to tests/storage_cosmos/test_cosmos_db_storage.py index 73c5644b..8a8ebf16 100644 --- a/libraries/microsoft-agents-storage-cosmos/tests/test_cosmos_db_storage.py +++ b/tests/storage_cosmos/test_cosmos_db_storage.py @@ -13,7 +13,7 @@ from microsoft_agents.storage.cosmos import CosmosDBStorage, CosmosDBStorageConfig from microsoft_agents.storage.cosmos.key_ops import sanitize_key -from microsoft_agents.hosting.core.storage._storage_test_utils import ( +from tests._common.storage.utils import ( QuickCRUDStorageTests, MockStoreItem, MockStoreItemB, diff --git a/libraries/microsoft-agents-storage-cosmos/tests/test_key_ops.py b/tests/storage_cosmos/test_key_ops.py similarity index 100% rename from libraries/microsoft-agents-storage-cosmos/tests/test_key_ops.py rename to tests/storage_cosmos/test_key_ops.py