diff --git a/mpt_api_client/resources/helpdesk/cases.py b/mpt_api_client/resources/helpdesk/cases.py new file mode 100644 index 0000000..1b150dc --- /dev/null +++ b/mpt_api_client/resources/helpdesk/cases.py @@ -0,0 +1,70 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncCreateMixin, + AsyncGetMixin, + AsyncUpdateMixin, + CollectionMixin, + CreateMixin, + GetMixin, + UpdateMixin, +) +from mpt_api_client.models import Model, ResourceData + + +class Case(Model): + """Helpdesk Case resource.""" + + +class CasesServiceConfig: + """Helpdesk Cases service configuration.""" + + _endpoint = "/public/v1/helpdesk/cases" + _model_class = Case + _collection_key = "data" + + +class CasesService( + CreateMixin[Case], + UpdateMixin[Case], + GetMixin[Case], + CollectionMixin[Case], + Service[Case], + CasesServiceConfig, +): + """Helpdesk Cases service.""" + + def query(self, resource_id: str, resource_data: ResourceData | None = None) -> Case: + """Switch case to query state.""" + return self._resource_action(resource_id, "POST", "query", json=resource_data) + + def process(self, resource_id: str, resource_data: ResourceData | None = None) -> Case: + """Switch case to process state.""" + return self._resource_action(resource_id, "POST", "process", json=resource_data) + + def complete(self, resource_id: str, resource_data: ResourceData | None = None) -> Case: + """Switch case to complete state.""" + return self._resource_action(resource_id, "POST", "complete", json=resource_data) + + +class AsyncCasesService( + AsyncCreateMixin[Case], + AsyncUpdateMixin[Case], + AsyncGetMixin[Case], + AsyncCollectionMixin[Case], + AsyncService[Case], + CasesServiceConfig, +): + """Async Helpdesk Cases service.""" + + async def query(self, resource_id: str, resource_data: ResourceData | None = None) -> Case: + """Switch case to query state.""" + return await self._resource_action(resource_id, "POST", "query", json=resource_data) + + async def process(self, resource_id: str, resource_data: ResourceData | None = None) -> Case: + """Switch case to process state.""" + return await self._resource_action(resource_id, "POST", "process", json=resource_data) + + async def complete(self, resource_id: str, resource_data: ResourceData | None = None) -> Case: + """Switch case to complete state.""" + return await self._resource_action(resource_id, "POST", "complete", json=resource_data) diff --git a/mpt_api_client/resources/helpdesk/helpdesk.py b/mpt_api_client/resources/helpdesk/helpdesk.py index 4d3541d..fd186d1 100644 --- a/mpt_api_client/resources/helpdesk/helpdesk.py +++ b/mpt_api_client/resources/helpdesk/helpdesk.py @@ -1,4 +1,5 @@ from mpt_api_client.http import AsyncHTTPClient, HTTPClient +from mpt_api_client.resources.helpdesk.cases import AsyncCasesService, CasesService from mpt_api_client.resources.helpdesk.chats import AsyncChatsService, ChatsService @@ -13,6 +14,11 @@ def chats(self) -> ChatsService: """Chats service.""" return ChatsService(http_client=self.http_client) + @property + def cases(self) -> CasesService: + """Cases service.""" + return CasesService(http_client=self.http_client) + class AsyncHelpdesk: """Async Helpdesk MPT API Module.""" @@ -24,3 +30,8 @@ def __init__(self, http_client: AsyncHTTPClient): def chats(self) -> AsyncChatsService: """Async Chats service.""" return AsyncChatsService(http_client=self.http_client) + + @property + def cases(self) -> AsyncCasesService: + """Async Cases service.""" + return AsyncCasesService(http_client=self.http_client) diff --git a/tests/e2e/helpdesk/cases/__init__.py b/tests/e2e/helpdesk/cases/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/e2e/helpdesk/cases/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/e2e/helpdesk/cases/conftest.py b/tests/e2e/helpdesk/cases/conftest.py new file mode 100644 index 0000000..ba66094 --- /dev/null +++ b/tests/e2e/helpdesk/cases/conftest.py @@ -0,0 +1,32 @@ +import pytest + + +@pytest.fixture +def invalid_case_id(): + return "CAS-0000-0000" + + +@pytest.fixture +def case_factory(short_uuid): + def factory( + title: str = "E2E Created Helpdesk Case", + description: str = "E2E Created Helpdesk Case Description", + ): + return { + "title": f"{title} {short_uuid}", + "description": description, + "priority": "Low", + "type": "General", + } + + return factory + + +@pytest.fixture +def created_case(mpt_ops, case_factory): + return mpt_ops.helpdesk.cases.create(case_factory()) + + +@pytest.fixture +async def async_created_case(async_mpt_ops, case_factory): + return await async_mpt_ops.helpdesk.cases.create(case_factory()) diff --git a/tests/e2e/helpdesk/cases/test_async_cases.py b/tests/e2e/helpdesk/cases/test_async_cases.py new file mode 100644 index 0000000..cd060d3 --- /dev/null +++ b/tests/e2e/helpdesk/cases/test_async_cases.py @@ -0,0 +1,71 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError + +pytestmark = [pytest.mark.flaky] + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_get_case(async_mpt_ops, async_created_case): + result = await async_mpt_ops.helpdesk.cases.get(async_created_case.id) + + assert result.id == async_created_case.id + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_list_cases(async_mpt_ops): + limit = 1 + + result = await async_mpt_ops.helpdesk.cases.fetch_page(limit=limit) + + assert len(result) > 0 + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_create_case(async_created_case): + result = async_created_case + + assert result is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_update_case(async_mpt_ops, async_created_case, short_uuid): + update_data = {"description": f"e2e update {short_uuid}"} + + result = await async_mpt_ops.helpdesk.cases.update(async_created_case.id, update_data) + + assert result.id == async_created_case.id + assert result.to_dict().get("description") == update_data["description"] + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_process_case(async_mpt_ops, async_created_case): + result = await async_mpt_ops.helpdesk.cases.process(async_created_case.id) + + assert result is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_query_case(async_mpt_ops, async_created_case): + processed_case = await async_mpt_ops.helpdesk.cases.process(async_created_case.id) + + result = await async_mpt_ops.helpdesk.cases.query( + processed_case.id, {"queryPrompt": "Could you provide more details?"} + ) + + assert result is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_complete_case(async_mpt_ops, async_created_case): + processed_case = await async_mpt_ops.helpdesk.cases.process(async_created_case.id) + + result = await async_mpt_ops.helpdesk.cases.complete(processed_case.id) + + assert result is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_not_found(async_mpt_ops, invalid_case_id): + with pytest.raises(MPTAPIError): + await async_mpt_ops.helpdesk.cases.get(invalid_case_id) diff --git a/tests/e2e/helpdesk/cases/test_sync_cases.py b/tests/e2e/helpdesk/cases/test_sync_cases.py new file mode 100644 index 0000000..dfd10d5 --- /dev/null +++ b/tests/e2e/helpdesk/cases/test_sync_cases.py @@ -0,0 +1,71 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError + +pytestmark = [pytest.mark.flaky] + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_get_case(mpt_ops, created_case): + result = mpt_ops.helpdesk.cases.get(created_case.id) + + assert result.id == created_case.id + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_list_cases(mpt_ops): + limit = 1 + + result = mpt_ops.helpdesk.cases.fetch_page(limit=limit) + + assert len(result) > 0 + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_create_case(created_case): + result = created_case + + assert result is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_update_case(mpt_ops, created_case, short_uuid): + update_data = {"description": f"e2e update {short_uuid}"} + + result = mpt_ops.helpdesk.cases.update(created_case.id, update_data) + + assert result.id == created_case.id + assert result.to_dict().get("description") == update_data["description"] + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_process_case(mpt_ops, created_case): + result = mpt_ops.helpdesk.cases.process(created_case.id) + + assert result is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_query_case(mpt_ops, created_case): + processed_case = mpt_ops.helpdesk.cases.process(created_case.id) + + result = mpt_ops.helpdesk.cases.query( + processed_case.id, {"queryPrompt": "Could you provide more details?"} + ) + + assert result is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_complete_case(mpt_ops, created_case): + processed_case = mpt_ops.helpdesk.cases.process(created_case.id) + + result = mpt_ops.helpdesk.cases.complete(processed_case.id) + + assert result is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_not_found(mpt_ops, invalid_case_id): + with pytest.raises(MPTAPIError): + mpt_ops.helpdesk.cases.get(invalid_case_id) diff --git a/tests/unit/resources/helpdesk/test_cases.py b/tests/unit/resources/helpdesk/test_cases.py new file mode 100644 index 0000000..4380d5a --- /dev/null +++ b/tests/unit/resources/helpdesk/test_cases.py @@ -0,0 +1,163 @@ +import httpx +import pytest +import respx + +from mpt_api_client.resources.helpdesk.cases import AsyncCasesService, Case, CasesService + + +def _request_content(action: str) -> bytes: + if action == "query": + return b'{"id":"CAS-1234-5678","queryPrompt":"Please provide more details"}' + if action == "process": + return b'{"id":"CAS-1234-5678","status":"Processing"}' + return b'{"id":"CAS-1234-5678","status":"Completed"}' + + +@pytest.fixture +def cases_service(http_client): + return CasesService(http_client=http_client) + + +@pytest.fixture +def async_cases_service(async_http_client): + return AsyncCasesService(http_client=async_http_client) + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "fetch_page", "query", "process", "complete"], +) +def test_methods_present(cases_service, method): + result = hasattr(cases_service, method) + + assert result is True + + +def test_delete_not_present(cases_service): + result = hasattr(cases_service, "delete") + + assert result is False + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "fetch_page", "query", "process", "complete"], +) +def test_async_methods_present(async_cases_service, method): + result = hasattr(async_cases_service, method) + + assert result is True + + +def test_async_delete_not_present(async_cases_service): + result = hasattr(async_cases_service, "delete") + + assert result is False + + +@pytest.mark.parametrize( + ("action", "input_data"), + [ + ("query", {"id": "CAS-1234-5678", "queryPrompt": "Please provide more details"}), + ("process", {"id": "CAS-1234-5678", "status": "Processing"}), + ("complete", {"id": "CAS-1234-5678", "status": "Completed"}), + ], +) +def test_custom_resource_actions(cases_service, action, input_data): + request_expected_content = _request_content(action) + response_expected_data = {"id": "CAS-1234-5678", "status": "Updated"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/helpdesk/cases/CAS-1234-5678/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(cases_service, action)("CAS-1234-5678", input_data) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.content == request_expected_content + assert result.to_dict() == response_expected_data + assert isinstance(result, Case) + + +@pytest.mark.parametrize("action", ["query", "process", "complete"]) +def test_custom_resource_actions_no_data(cases_service, action): + response_expected_data = {"id": "CAS-1234-5678", "status": "Updated"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/helpdesk/cases/CAS-1234-5678/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(cases_service, action)("CAS-1234-5678") + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.content == b"" + assert result.to_dict() == response_expected_data + assert isinstance(result, Case) + + +@pytest.mark.parametrize( + ("action", "input_data"), + [ + ("query", {"id": "CAS-1234-5678", "queryPrompt": "Please provide more details"}), + ("process", {"id": "CAS-1234-5678", "status": "Processing"}), + ("complete", {"id": "CAS-1234-5678", "status": "Completed"}), + ], +) +async def test_async_custom_resource_actions(async_cases_service, action, input_data): + request_expected_content = _request_content(action) + response_expected_data = {"id": "CAS-1234-5678", "status": "Updated"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/helpdesk/cases/CAS-1234-5678/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = await getattr(async_cases_service, action)("CAS-1234-5678", input_data) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.content == request_expected_content + assert result.to_dict() == response_expected_data + assert isinstance(result, Case) + + +@pytest.mark.parametrize("action", ["query", "process", "complete"]) +async def test_async_custom_resource_actions_no_data(async_cases_service, action): + response_expected_data = {"id": "CAS-1234-5678", "status": "Updated"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/helpdesk/cases/CAS-1234-5678/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = await getattr(async_cases_service, action)("CAS-1234-5678") + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.content == b"" + assert result.to_dict() == response_expected_data + assert isinstance(result, Case) diff --git a/tests/unit/resources/helpdesk/test_helpdesk.py b/tests/unit/resources/helpdesk/test_helpdesk.py index d4113f9..6aed23c 100644 --- a/tests/unit/resources/helpdesk/test_helpdesk.py +++ b/tests/unit/resources/helpdesk/test_helpdesk.py @@ -1,6 +1,7 @@ import pytest from mpt_api_client.resources.helpdesk import AsyncHelpdesk, Helpdesk +from mpt_api_client.resources.helpdesk.cases import AsyncCasesService, CasesService from mpt_api_client.resources.helpdesk.chats import AsyncChatsService, ChatsService @@ -22,6 +23,7 @@ def test_async_helpdesk_init(async_http_client): ("attr_name", "expected"), [ ("chats", ChatsService), + ("cases", CasesService), ], ) def test_helpdesk_properties(http_client, attr_name, expected): @@ -36,6 +38,7 @@ def test_helpdesk_properties(http_client, attr_name, expected): ("attr_name", "expected"), [ ("chats", AsyncChatsService), + ("cases", AsyncCasesService), ], ) def test_async_helpdesk_properties(async_http_client, attr_name, expected):