From b8d7e545f53436806f2b8eeae10babbba39432df Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:12:34 +1200 Subject: [PATCH 1/4] fix(freshdesk): upgrade to SDK 2.0.0, add unit tests - Update SDK to 2.0.0: all context.fetch() calls access .data - Import and use ActionResult/ActionError return types - Fix auth: use flat context.auth dict (custom auth type) - Remove legacy result/error fields from all output schemas - Bump config.json version to 2.0.0 - Add 88 pytest unit tests covering all 17 actions --- freshdesk/config.json | 204 +----- freshdesk/freshdesk.py | 101 ++- freshdesk/requirements.txt | 2 +- freshdesk/tests/conftest.py | 4 + freshdesk/tests/test_freshdesk_unit.py | 958 +++++++++++++++++++++++++ 5 files changed, 1041 insertions(+), 228 deletions(-) create mode 100644 freshdesk/tests/conftest.py create mode 100644 freshdesk/tests/test_freshdesk_unit.py diff --git a/freshdesk/config.json b/freshdesk/config.json index b7d3f77b..a89aa0c7 100644 --- a/freshdesk/config.json +++ b/freshdesk/config.json @@ -1,7 +1,7 @@ { "name": "Freshdesk", "display_name": "Freshdesk", - "version": "1.0.0", + "version": "2.0.0", "description": "Freshdesk integration for managing tickets, contacts, companies, and customer support operations", "entry_point": "freshdesk.py", "auth": { @@ -91,17 +91,9 @@ "total": { "type": "integer", "description": "Total number of companies" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["companies", "result"] + "required": ["companies", "total"] } }, "create_company": { @@ -175,17 +167,9 @@ "description": "Company last update timestamp" } } - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["company", "result"] + "required": ["company"] } }, "get_company": { @@ -240,17 +224,9 @@ "description": "Company last update timestamp" } } - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["company", "result"] + "required": ["company"] } }, "update_company": { @@ -328,17 +304,9 @@ "description": "Company last update timestamp" } } - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["company", "result"] + "required": ["company"] } }, "delete_company": { @@ -357,16 +325,12 @@ "output_schema": { "type": "object", "properties": { - "result": { + "deleted": { "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" + "description": "Whether the company was deleted" } }, - "required": ["result"] + "required": ["deleted"] } }, "search_companies": { @@ -405,17 +369,9 @@ "total": { "type": "integer", "description": "Total number of companies found" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["companies", "result"] + "required": ["companies", "total"] } }, "create_ticket": { @@ -475,17 +431,9 @@ "ticket": { "type": "object", "description": "Created ticket details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["ticket", "result"] + "required": ["ticket"] } }, "list_tickets": { @@ -518,17 +466,9 @@ "total": { "type": "integer", "description": "Total number of tickets returned" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["tickets", "result"] + "required": ["tickets", "total"] } }, "get_ticket": { @@ -550,17 +490,9 @@ "ticket": { "type": "object", "description": "Ticket details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["ticket", "result"] + "required": ["ticket"] } }, "update_ticket": { @@ -607,17 +539,9 @@ "ticket": { "type": "object", "description": "Updated ticket details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["ticket", "result"] + "required": ["ticket"] } }, "delete_ticket": { @@ -636,16 +560,12 @@ "output_schema": { "type": "object", "properties": { - "result": { + "deleted": { "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" + "description": "Whether the ticket was deleted" } }, - "required": ["result"] + "required": ["deleted"] } }, "create_contact": { @@ -698,17 +618,9 @@ "contact": { "type": "object", "description": "Created contact details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["contact", "result"] + "required": ["contact"] } }, "list_contacts": { @@ -741,17 +653,9 @@ "total": { "type": "integer", "description": "Total number of contacts returned" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["contacts", "result"] + "required": ["contacts", "total"] } }, "get_contact": { @@ -773,17 +677,9 @@ "contact": { "type": "object", "description": "Contact details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["contact", "result"] + "required": ["contact"] } }, "update_contact": { @@ -829,17 +725,9 @@ "contact": { "type": "object", "description": "Updated contact details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["contact", "result"] + "required": ["contact"] } }, "delete_contact": { @@ -858,16 +746,12 @@ "output_schema": { "type": "object", "properties": { - "result": { + "deleted": { "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" + "description": "Whether the contact was deleted" } }, - "required": ["result"] + "required": ["deleted"] } }, "search_contacts": { @@ -906,17 +790,9 @@ "total": { "type": "integer", "description": "Total number of contacts found" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["contacts", "result"] + "required": ["contacts", "total"] } }, "list_conversations": { @@ -938,17 +814,9 @@ "conversations": { "type": "array", "description": "List of conversations" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["conversations", "result"] + "required": ["conversations"] } }, "create_note": { @@ -981,17 +849,9 @@ "conversation": { "type": "object", "description": "Created note details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["conversation", "result"] + "required": ["conversation"] } }, "create_reply": { @@ -1021,18 +881,10 @@ "conversation": { "type": "object", "description": "Created reply details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": "string", - "description": "Error message if the action failed" } }, - "required": ["conversation", "result"] + "required": ["conversation"] } } } -} \ No newline at end of file +} diff --git a/freshdesk/freshdesk.py b/freshdesk/freshdesk.py index c86edd45..a838be21 100644 --- a/freshdesk/freshdesk.py +++ b/freshdesk/freshdesk.py @@ -1,4 +1,4 @@ -from autohive_integrations_sdk import Integration, ExecutionContext, ActionHandler +from autohive_integrations_sdk import Integration, ExecutionContext, ActionHandler, ActionResult, ActionError from typing import Dict, Any import base64 @@ -23,8 +23,7 @@ def get_auth_headers(context: ExecutionContext) -> Dict[str, str]: Returns: Dictionary with Authorization and Content-Type headers """ - credentials = context.auth.get("credentials", {}) - api_key = credentials.get("api_key", "") + api_key = context.auth.get("api_key", "") # Freshdesk requires Basic Auth with format: api_key:X auth_string = f"{api_key}:X" @@ -44,8 +43,7 @@ def get_base_url(context: ExecutionContext) -> str: Returns: Base URL string (e.g., https://yourcompany.freshdesk.com/api/v2) """ - credentials = context.auth.get("credentials", {}) - domain = credentials.get("domain", "") + domain = context.auth.get("domain", "") return f"https://{domain}.freshdesk.com/api/{FRESHDESK_API_VERSION}" @@ -78,12 +76,12 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(url, method="GET", headers=headers, params=params) # Response is a list of companies - companies = response if isinstance(response, list) else [] + companies = response.data if isinstance(response.data, list) else [] - return {"companies": companies, "total": len(companies), "result": True} + return ActionResult(data={"companies": companies, "total": len(companies)}) except Exception as e: - return {"companies": [], "total": 0, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("create_company") @@ -119,10 +117,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): url = f"{base_url}/companies" response = await context.fetch(url, method="POST", headers=headers, json=body) - return {"company": response, "result": True} + return ActionResult(data={"company": response.data}) except Exception as e: - return {"company": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("get_company") @@ -144,10 +142,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): url = f"{base_url}/companies/{company_id}" response = await context.fetch(url, method="GET", headers=headers) - return {"company": response, "result": True} + return ActionResult(data={"company": response.data}) except Exception as e: - return {"company": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("update_company") @@ -188,10 +186,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): url = f"{base_url}/companies/{company_id}" response = await context.fetch(url, method="PUT", headers=headers, json=body) - return {"company": response, "result": True} + return ActionResult(data={"company": response.data}) except Exception as e: - return {"company": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("delete_company") @@ -214,10 +212,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): url = f"{base_url}/companies/{company_id}" await context.fetch(url, method="DELETE", headers=headers) - return {"result": True} + return ActionResult(data={"deleted": True}) except Exception as e: - return {"result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("search_companies") @@ -246,12 +244,13 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(url, method="GET", headers=headers, params=params) # Extract companies from response - companies = response.get("companies", []) if isinstance(response, dict) else [] + body = response.data + companies = body.get("companies", []) if isinstance(body, dict) else [] - return {"companies": companies, "total": len(companies), "result": True} + return ActionResult(data={"companies": companies, "total": len(companies)}) except Exception as e: - return {"companies": [], "total": 0, "result": False, "error": str(e)} + return ActionError(message=str(e)) # ---- Ticket Handlers ---- @@ -285,10 +284,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(f"{base_url}/tickets", method="POST", headers=headers, json=body) - return {"ticket": response, "result": True} + return ActionResult(data={"ticket": response.data}) except Exception as e: - return {"ticket": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("list_tickets") @@ -304,12 +303,12 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(f"{base_url}/tickets", method="GET", headers=headers, params=params) - tickets = response if isinstance(response, list) else [] + tickets = response.data if isinstance(response.data, list) else [] - return {"tickets": tickets, "total": len(tickets), "result": True} + return ActionResult(data={"tickets": tickets, "total": len(tickets)}) except Exception as e: - return {"tickets": [], "total": 0, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("get_ticket") @@ -325,10 +324,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(f"{base_url}/tickets/{ticket_id}", method="GET", headers=headers) - return {"ticket": response, "result": True} + return ActionResult(data={"ticket": response.data}) except Exception as e: - return {"ticket": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("update_ticket") @@ -356,10 +355,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(f"{base_url}/tickets/{ticket_id}", method="PUT", headers=headers, json=body) - return {"ticket": response, "result": True} + return ActionResult(data={"ticket": response.data}) except Exception as e: - return {"ticket": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("delete_ticket") @@ -375,10 +374,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): await context.fetch(f"{base_url}/tickets/{ticket_id}", method="DELETE", headers=headers) - return {"result": True} + return ActionResult(data={"deleted": True}) except Exception as e: - return {"result": False, "error": str(e)} + return ActionError(message=str(e)) # ---- Contact Handlers ---- @@ -410,10 +409,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(f"{base_url}/contacts", method="POST", headers=headers, json=body) - return {"contact": response, "result": True} + return ActionResult(data={"contact": response.data}) except Exception as e: - return {"contact": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("list_contacts") @@ -429,12 +428,12 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(f"{base_url}/contacts", method="GET", headers=headers, params=params) - contacts = response if isinstance(response, list) else [] + contacts = response.data if isinstance(response.data, list) else [] - return {"contacts": contacts, "total": len(contacts), "result": True} + return ActionResult(data={"contacts": contacts, "total": len(contacts)}) except Exception as e: - return {"contacts": [], "total": 0, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("get_contact") @@ -450,10 +449,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(f"{base_url}/contacts/{contact_id}", method="GET", headers=headers) - return {"contact": response, "result": True} + return ActionResult(data={"contact": response.data}) except Exception as e: - return {"contact": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("update_contact") @@ -485,10 +484,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): f"{base_url}/contacts/{contact_id}", method="PUT", headers=headers, json=body ) - return {"contact": response, "result": True} + return ActionResult(data={"contact": response.data}) except Exception as e: - return {"contact": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("delete_contact") @@ -504,10 +503,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): await context.fetch(f"{base_url}/contacts/{contact_id}", method="DELETE", headers=headers) - return {"result": True} + return ActionResult(data={"deleted": True}) except Exception as e: - return {"result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("search_contacts") @@ -536,12 +535,12 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): response = await context.fetch(url, method="GET", headers=headers, params=params) # Response is directly an array of contacts - contacts = response if isinstance(response, list) else [] + contacts = response.data if isinstance(response.data, list) else [] - return {"contacts": contacts, "total": len(contacts), "result": True} + return ActionResult(data={"contacts": contacts, "total": len(contacts)}) except Exception as e: - return {"contacts": [], "total": 0, "result": False, "error": str(e)} + return ActionError(message=str(e)) # ---- Conversation Handlers ---- @@ -562,12 +561,12 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): f"{base_url}/tickets/{ticket_id}/conversations", method="GET", headers=headers ) - conversations = response if isinstance(response, list) else [] + conversations = response.data if isinstance(response.data, list) else [] - return {"conversations": conversations, "result": True} + return ActionResult(data={"conversations": conversations}) except Exception as e: - return {"conversations": [], "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("create_note") @@ -589,10 +588,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): f"{base_url}/tickets/{ticket_id}/notes", method="POST", headers=headers, json=body ) - return {"conversation": response, "result": True} + return ActionResult(data={"conversation": response.data}) except Exception as e: - return {"conversation": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) @freshdesk.action("create_reply") @@ -614,7 +613,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): f"{base_url}/tickets/{ticket_id}/reply", method="POST", headers=headers, json=body ) - return {"conversation": response, "result": True} + return ActionResult(data={"conversation": response.data}) except Exception as e: - return {"conversation": {}, "result": False, "error": str(e)} + return ActionError(message=str(e)) diff --git a/freshdesk/requirements.txt b/freshdesk/requirements.txt index b56fee2e..1af9591f 100644 --- a/freshdesk/requirements.txt +++ b/freshdesk/requirements.txt @@ -1 +1 @@ -autohive-integrations-sdk~=1.0.2 +autohive-integrations-sdk~=2.0.0 diff --git a/freshdesk/tests/conftest.py b/freshdesk/tests/conftest.py new file mode 100644 index 00000000..306f2d40 --- /dev/null +++ b/freshdesk/tests/conftest.py @@ -0,0 +1,4 @@ +import sys +import os + +sys.path.insert(0, os.path.dirname(__file__)) diff --git a/freshdesk/tests/test_freshdesk_unit.py b/freshdesk/tests/test_freshdesk_unit.py new file mode 100644 index 00000000..5a2eca1c --- /dev/null +++ b/freshdesk/tests/test_freshdesk_unit.py @@ -0,0 +1,958 @@ +import os +import sys +import importlib.util + +_parent = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +_deps = os.path.abspath(os.path.join(os.path.dirname(__file__), "../dependencies")) +sys.path.insert(0, _parent) +sys.path.insert(0, _deps) + +import pytest # noqa: E402 +from unittest.mock import AsyncMock, MagicMock # noqa: E402 +from autohive_integrations_sdk import FetchResponse # noqa: E402 +from autohive_integrations_sdk.integration import ResultType # noqa: E402 + +_spec = importlib.util.spec_from_file_location("freshdesk_mod", os.path.join(_parent, "freshdesk.py")) +_mod = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_mod) + +freshdesk = _mod.freshdesk # the Integration instance +get_auth_headers = _mod.get_auth_headers +get_base_url = _mod.get_base_url + +pytestmark = pytest.mark.unit + + +# ---- Fixtures ---- + + +@pytest.fixture +def mock_context(): + ctx = MagicMock(name="ExecutionContext") + ctx.fetch = AsyncMock(name="fetch") + ctx.auth = {"api_key": "test_api_key", "domain": "testcompany"} # nosec B105 + return ctx + + +# ---- Sample Data ---- + +SAMPLE_COMPANY = { + "id": 1001, + "name": "Acme Corp", + "description": "A test company", + "domains": ["acme.com"], + "note": "VIP customer", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-02T00:00:00Z", +} + +SAMPLE_TICKET = { + "id": 5001, + "subject": "Test issue", + "description": "
Something broke
", + "status": 2, + "priority": 1, + "email": "customer@example.com", + "created_at": "2024-01-01T00:00:00Z", +} + +SAMPLE_CONTACT = { + "id": 3001, + "name": "John Doe", + "email": "john@example.com", + "phone": "555-0100", + "created_at": "2024-01-01T00:00:00Z", +} + +SAMPLE_CONVERSATION = { + "id": 7001, + "body": "Note content
", + "private": True, + "created_at": "2024-01-01T00:00:00Z", +} + + +# ---- Helper Function Tests ---- + + +class TestGetAuthHeaders: + def test_returns_authorization_header(self, mock_context): + headers = get_auth_headers(mock_context) + assert "Authorization" in headers + assert headers["Authorization"].startswith("Basic ") + + def test_returns_content_type_header(self, mock_context): + headers = get_auth_headers(mock_context) + assert headers["Content-Type"] == "application/json" + + def test_uses_api_key_from_auth(self, mock_context): + import base64 + + headers = get_auth_headers(mock_context) + encoded = headers["Authorization"].replace("Basic ", "") + decoded = base64.b64decode(encoded).decode("ascii") + assert decoded == "test_api_key:X" # nosec B105 + + +class TestGetBaseUrl: + def test_includes_domain(self, mock_context): + url = get_base_url(mock_context) + assert "testcompany" in url + + def test_freshdesk_subdomain_format(self, mock_context): + url = get_base_url(mock_context) + assert url == "https://testcompany.freshdesk.com/api/v2" + + +# ---- Company Tests ---- + + +class TestListCompanies: + @pytest.mark.asyncio + async def test_happy_path_returns_companies(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[SAMPLE_COMPANY]) + + result = await freshdesk.execute_action("list_companies", {}, mock_context) + + assert result.result.data["companies"] == [SAMPLE_COMPANY] + assert result.result.data["total"] == 1 + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("list_companies", {}, mock_context) + + call_args = mock_context.fetch.call_args + assert "testcompany.freshdesk.com/api/v2/companies" in call_args.args[0] + assert call_args.kwargs["method"] == "GET" + + @pytest.mark.asyncio + async def test_default_pagination_params(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("list_companies", {}, mock_context) + + params = mock_context.fetch.call_args.kwargs["params"] + assert params["page"] == 1 + assert params["per_page"] == 30 + + @pytest.mark.asyncio + async def test_custom_pagination_params(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("list_companies", {"page": 2, "per_page": 10}, mock_context) + + params = mock_context.fetch.call_args.kwargs["params"] + assert params["page"] == 2 + assert params["per_page"] == 10 + + @pytest.mark.asyncio + async def test_non_list_response_returns_empty(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data={"error": "bad"}) + + result = await freshdesk.execute_action("list_companies", {}, mock_context) + + assert result.result.data["companies"] == [] + assert result.result.data["total"] == 0 + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Network error") + + result = await freshdesk.execute_action("list_companies", {}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Network error" in result.result.message + + +class TestCreateCompany: + @pytest.mark.asyncio + async def test_happy_path_returns_company(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_COMPANY) + + result = await freshdesk.execute_action("create_company", {"name": "Acme Corp"}, mock_context) + + assert result.result.data["company"] == SAMPLE_COMPANY + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_COMPANY) + + await freshdesk.execute_action("create_company", {"name": "Acme Corp"}, mock_context) + + call_args = mock_context.fetch.call_args + assert "testcompany.freshdesk.com/api/v2/companies" in call_args.args[0] + assert call_args.kwargs["method"] == "POST" + + @pytest.mark.asyncio + async def test_name_included_in_body(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_COMPANY) + + await freshdesk.execute_action("create_company", {"name": "Acme Corp"}, mock_context) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["name"] == "Acme Corp" + + @pytest.mark.asyncio + async def test_optional_fields_included_when_provided(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_COMPANY) + + await freshdesk.execute_action( + "create_company", + {"name": "Acme Corp", "description": "Test desc", "domains": ["acme.com"], "note": "VIP"}, + mock_context, + ) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["description"] == "Test desc" + assert body["domains"] == ["acme.com"] + assert body["note"] == "VIP" + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("API error") + + result = await freshdesk.execute_action("create_company", {"name": "Acme Corp"}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "API error" in result.result.message + + +class TestGetCompany: + @pytest.mark.asyncio + async def test_happy_path_returns_company(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_COMPANY) + + result = await freshdesk.execute_action("get_company", {"company_id": 1001}, mock_context) + + assert result.result.data["company"] == SAMPLE_COMPANY + + @pytest.mark.asyncio + async def test_request_url_includes_company_id(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_COMPANY) + + await freshdesk.execute_action("get_company", {"company_id": 1001}, mock_context) + + url = mock_context.fetch.call_args.args[0] + assert "/companies/1001" in url + + @pytest.mark.asyncio + async def test_request_method_is_get(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_COMPANY) + + await freshdesk.execute_action("get_company", {"company_id": 1001}, mock_context) + + assert mock_context.fetch.call_args.kwargs["method"] == "GET" + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Not found") + + result = await freshdesk.execute_action("get_company", {"company_id": 9999}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Not found" in result.result.message + + +class TestUpdateCompany: + @pytest.mark.asyncio + async def test_happy_path_returns_company(self, mock_context): + updated = {**SAMPLE_COMPANY, "name": "New Name"} + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=updated) + + result = await freshdesk.execute_action( + "update_company", {"company_id": 1001, "name": "New Name"}, mock_context + ) + + assert result.result.data["company"]["name"] == "New Name" + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_COMPANY) + + await freshdesk.execute_action("update_company", {"company_id": 1001, "name": "New Name"}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/companies/1001" in call_args.args[0] + assert call_args.kwargs["method"] == "PUT" + + @pytest.mark.asyncio + async def test_only_provided_fields_in_body(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_COMPANY) + + await freshdesk.execute_action("update_company", {"company_id": 1001, "name": "New Name"}, mock_context) + + body = mock_context.fetch.call_args.kwargs["json"] + assert "name" in body + assert "description" not in body + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Update failed") + + result = await freshdesk.execute_action("update_company", {"company_id": 1001}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Update failed" in result.result.message + + +class TestDeleteCompany: + @pytest.mark.asyncio + async def test_happy_path_returns_deleted_true(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=204, headers={}, data=None) + + result = await freshdesk.execute_action("delete_company", {"company_id": 1001}, mock_context) + + assert result.result.data["deleted"] is True + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=204, headers={}, data=None) + + await freshdesk.execute_action("delete_company", {"company_id": 1001}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/companies/1001" in call_args.args[0] + assert call_args.kwargs["method"] == "DELETE" + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Forbidden") + + result = await freshdesk.execute_action("delete_company", {"company_id": 1001}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Forbidden" in result.result.message + + +class TestSearchCompanies: + @pytest.mark.asyncio + async def test_happy_path_returns_companies(self, mock_context): + mock_context.fetch.return_value = FetchResponse( + status=200, headers={}, data={"companies": [{"id": 1001, "name": "Acme Corp"}]} + ) + + result = await freshdesk.execute_action("search_companies", {"name": "Acme"}, mock_context) + + assert len(result.result.data["companies"]) == 1 + assert result.result.data["total"] == 1 + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data={"companies": []}) + + await freshdesk.execute_action("search_companies", {"name": "Acme"}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/companies/autocomplete" in call_args.args[0] + assert call_args.kwargs["method"] == "GET" + + @pytest.mark.asyncio + async def test_name_passed_as_param(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data={"companies": []}) + + await freshdesk.execute_action("search_companies", {"name": "Acme"}, mock_context) + + params = mock_context.fetch.call_args.kwargs["params"] + assert params["name"] == "Acme" + + @pytest.mark.asyncio + async def test_no_companies_key_returns_empty(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data={}) + + result = await freshdesk.execute_action("search_companies", {"name": "Acme"}, mock_context) + + assert result.result.data["companies"] == [] + assert result.result.data["total"] == 0 + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Search failed") + + result = await freshdesk.execute_action("search_companies", {"name": "Acme"}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Search failed" in result.result.message + + +# ---- Ticket Tests ---- + + +class TestCreateTicket: + @pytest.mark.asyncio + async def test_happy_path_returns_ticket(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_TICKET) + + result = await freshdesk.execute_action( + "create_ticket", {"subject": "Test issue", "email": "customer@example.com"}, mock_context + ) + + assert result.result.data["ticket"] == SAMPLE_TICKET + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_TICKET) + + await freshdesk.execute_action( + "create_ticket", {"subject": "Test issue", "email": "customer@example.com"}, mock_context + ) + + call_args = mock_context.fetch.call_args + assert "testcompany.freshdesk.com/api/v2/tickets" in call_args.args[0] + assert call_args.kwargs["method"] == "POST" + + @pytest.mark.asyncio + async def test_required_fields_in_body(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_TICKET) + + await freshdesk.execute_action( + "create_ticket", {"subject": "Test issue", "email": "customer@example.com"}, mock_context + ) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["subject"] == "Test issue" + assert body["email"] == "customer@example.com" + + @pytest.mark.asyncio + async def test_optional_fields_included_when_provided(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_TICKET) + + await freshdesk.execute_action( + "create_ticket", + {"subject": "Test", "email": "c@example.com", "priority": 2, "status": 2, "tags": ["bug"]}, + mock_context, + ) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["priority"] == 2 + assert body["status"] == 2 + assert body["tags"] == ["bug"] + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Create ticket failed") + + result = await freshdesk.execute_action( + "create_ticket", {"subject": "Test", "email": "c@example.com"}, mock_context + ) + + assert result.type == ResultType.ACTION_ERROR + assert "Create ticket failed" in result.result.message + + +class TestListTickets: + @pytest.mark.asyncio + async def test_happy_path_returns_tickets(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[SAMPLE_TICKET]) + + result = await freshdesk.execute_action("list_tickets", {}, mock_context) + + assert result.result.data["tickets"] == [SAMPLE_TICKET] + assert result.result.data["total"] == 1 + + @pytest.mark.asyncio + async def test_request_method_is_get(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("list_tickets", {}, mock_context) + + assert mock_context.fetch.call_args.kwargs["method"] == "GET" + + @pytest.mark.asyncio + async def test_default_pagination(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("list_tickets", {}, mock_context) + + params = mock_context.fetch.call_args.kwargs["params"] + assert params["page"] == 1 + assert params["per_page"] == 30 + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("List failed") + + result = await freshdesk.execute_action("list_tickets", {}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "List failed" in result.result.message + + +class TestGetTicket: + @pytest.mark.asyncio + async def test_happy_path_returns_ticket(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_TICKET) + + result = await freshdesk.execute_action("get_ticket", {"ticket_id": 5001}, mock_context) + + assert result.result.data["ticket"] == SAMPLE_TICKET + + @pytest.mark.asyncio + async def test_request_url_includes_ticket_id(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_TICKET) + + await freshdesk.execute_action("get_ticket", {"ticket_id": 5001}, mock_context) + + assert "/tickets/5001" in mock_context.fetch.call_args.args[0] + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Ticket not found") + + result = await freshdesk.execute_action("get_ticket", {"ticket_id": 9999}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Ticket not found" in result.result.message + + +class TestUpdateTicket: + @pytest.mark.asyncio + async def test_happy_path_returns_ticket(self, mock_context): + updated = {**SAMPLE_TICKET, "status": 4} + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=updated) + + result = await freshdesk.execute_action("update_ticket", {"ticket_id": 5001, "status": 4}, mock_context) + + assert result.result.data["ticket"]["status"] == 4 + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_TICKET) + + await freshdesk.execute_action("update_ticket", {"ticket_id": 5001, "status": 4}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/tickets/5001" in call_args.args[0] + assert call_args.kwargs["method"] == "PUT" + + @pytest.mark.asyncio + async def test_only_provided_fields_in_body(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_TICKET) + + await freshdesk.execute_action("update_ticket", {"ticket_id": 5001, "status": 4}, mock_context) + + body = mock_context.fetch.call_args.kwargs["json"] + assert "status" in body + assert "subject" not in body + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Update failed") + + result = await freshdesk.execute_action("update_ticket", {"ticket_id": 5001}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Update failed" in result.result.message + + +class TestDeleteTicket: + @pytest.mark.asyncio + async def test_happy_path_returns_deleted_true(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=204, headers={}, data=None) + + result = await freshdesk.execute_action("delete_ticket", {"ticket_id": 5001}, mock_context) + + assert result.result.data["deleted"] is True + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=204, headers={}, data=None) + + await freshdesk.execute_action("delete_ticket", {"ticket_id": 5001}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/tickets/5001" in call_args.args[0] + assert call_args.kwargs["method"] == "DELETE" + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Delete failed") + + result = await freshdesk.execute_action("delete_ticket", {"ticket_id": 5001}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Delete failed" in result.result.message + + +# ---- Contact Tests ---- + + +class TestCreateContact: + @pytest.mark.asyncio + async def test_happy_path_returns_contact(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONTACT) + + result = await freshdesk.execute_action( + "create_contact", {"name": "John Doe", "email": "john@example.com"}, mock_context + ) + + assert result.result.data["contact"] == SAMPLE_CONTACT + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONTACT) + + await freshdesk.execute_action( + "create_contact", {"name": "John Doe", "email": "john@example.com"}, mock_context + ) + + call_args = mock_context.fetch.call_args + assert "testcompany.freshdesk.com/api/v2/contacts" in call_args.args[0] + assert call_args.kwargs["method"] == "POST" + + @pytest.mark.asyncio + async def test_required_fields_in_body(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONTACT) + + await freshdesk.execute_action( + "create_contact", {"name": "John Doe", "email": "john@example.com"}, mock_context + ) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["name"] == "John Doe" + assert body["email"] == "john@example.com" + + @pytest.mark.asyncio + async def test_optional_fields_included_when_provided(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONTACT) + + await freshdesk.execute_action( + "create_contact", + {"name": "John Doe", "email": "john@example.com", "phone": "555-0100", "job_title": "Engineer"}, + mock_context, + ) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["phone"] == "555-0100" + assert body["job_title"] == "Engineer" + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Duplicate email") + + result = await freshdesk.execute_action( + "create_contact", {"name": "John Doe", "email": "john@example.com"}, mock_context + ) + + assert result.type == ResultType.ACTION_ERROR + assert "Duplicate email" in result.result.message + + +class TestListContacts: + @pytest.mark.asyncio + async def test_happy_path_returns_contacts(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[SAMPLE_CONTACT]) + + result = await freshdesk.execute_action("list_contacts", {}, mock_context) + + assert result.result.data["contacts"] == [SAMPLE_CONTACT] + assert result.result.data["total"] == 1 + + @pytest.mark.asyncio + async def test_non_list_response_returns_empty(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=None) + + result = await freshdesk.execute_action("list_contacts", {}, mock_context) + + assert result.result.data["contacts"] == [] + assert result.result.data["total"] == 0 + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("List contacts failed") + + result = await freshdesk.execute_action("list_contacts", {}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "List contacts failed" in result.result.message + + +class TestGetContact: + @pytest.mark.asyncio + async def test_happy_path_returns_contact(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_CONTACT) + + result = await freshdesk.execute_action("get_contact", {"contact_id": 3001}, mock_context) + + assert result.result.data["contact"] == SAMPLE_CONTACT + + @pytest.mark.asyncio + async def test_request_url_includes_contact_id(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_CONTACT) + + await freshdesk.execute_action("get_contact", {"contact_id": 3001}, mock_context) + + assert "/contacts/3001" in mock_context.fetch.call_args.args[0] + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Contact not found") + + result = await freshdesk.execute_action("get_contact", {"contact_id": 9999}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Contact not found" in result.result.message + + +class TestUpdateContact: + @pytest.mark.asyncio + async def test_happy_path_returns_contact(self, mock_context): + updated = {**SAMPLE_CONTACT, "job_title": "Senior Engineer"} + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=updated) + + result = await freshdesk.execute_action( + "update_contact", {"contact_id": 3001, "job_title": "Senior Engineer"}, mock_context + ) + + assert result.result.data["contact"]["job_title"] == "Senior Engineer" + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=SAMPLE_CONTACT) + + await freshdesk.execute_action("update_contact", {"contact_id": 3001, "name": "Jane Doe"}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/contacts/3001" in call_args.args[0] + assert call_args.kwargs["method"] == "PUT" + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Update contact failed") + + result = await freshdesk.execute_action("update_contact", {"contact_id": 3001}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Update contact failed" in result.result.message + + +class TestDeleteContact: + @pytest.mark.asyncio + async def test_happy_path_returns_deleted_true(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=204, headers={}, data=None) + + result = await freshdesk.execute_action("delete_contact", {"contact_id": 3001}, mock_context) + + assert result.result.data["deleted"] is True + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=204, headers={}, data=None) + + await freshdesk.execute_action("delete_contact", {"contact_id": 3001}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/contacts/3001" in call_args.args[0] + assert call_args.kwargs["method"] == "DELETE" + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Delete contact failed") + + result = await freshdesk.execute_action("delete_contact", {"contact_id": 3001}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Delete contact failed" in result.result.message + + +class TestSearchContacts: + @pytest.mark.asyncio + async def test_happy_path_returns_contacts(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[{"id": 3001, "name": "John Doe"}]) + + result = await freshdesk.execute_action("search_contacts", {"term": "John"}, mock_context) + + assert len(result.result.data["contacts"]) == 1 + assert result.result.data["total"] == 1 + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("search_contacts", {"term": "John"}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/contacts/autocomplete" in call_args.args[0] + assert call_args.kwargs["method"] == "GET" + + @pytest.mark.asyncio + async def test_term_passed_as_param(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("search_contacts", {"term": "John"}, mock_context) + + params = mock_context.fetch.call_args.kwargs["params"] + assert params["term"] == "John" + + @pytest.mark.asyncio + async def test_non_list_returns_empty(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data={"error": "oops"}) + + result = await freshdesk.execute_action("search_contacts", {"term": "John"}, mock_context) + + assert result.result.data["contacts"] == [] + assert result.result.data["total"] == 0 + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Search failed") + + result = await freshdesk.execute_action("search_contacts", {"term": "John"}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Search failed" in result.result.message + + +# ---- Conversation Tests ---- + + +class TestListConversations: + @pytest.mark.asyncio + async def test_happy_path_returns_conversations(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[SAMPLE_CONVERSATION]) + + result = await freshdesk.execute_action("list_conversations", {"ticket_id": 5001}, mock_context) + + assert result.result.data["conversations"] == [SAMPLE_CONVERSATION] + + @pytest.mark.asyncio + async def test_request_url_includes_ticket_id(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("list_conversations", {"ticket_id": 5001}, mock_context) + + assert "/tickets/5001/conversations" in mock_context.fetch.call_args.args[0] + + @pytest.mark.asyncio + async def test_request_method_is_get(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=[]) + + await freshdesk.execute_action("list_conversations", {"ticket_id": 5001}, mock_context) + + assert mock_context.fetch.call_args.kwargs["method"] == "GET" + + @pytest.mark.asyncio + async def test_non_list_response_returns_empty(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=200, headers={}, data=None) + + result = await freshdesk.execute_action("list_conversations", {"ticket_id": 5001}, mock_context) + + assert result.result.data["conversations"] == [] + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Conversations failed") + + result = await freshdesk.execute_action("list_conversations", {"ticket_id": 5001}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Conversations failed" in result.result.message + + +class TestCreateNote: + @pytest.mark.asyncio + async def test_happy_path_returns_conversation(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONVERSATION) + + result = await freshdesk.execute_action("create_note", {"ticket_id": 5001, "body": "Note
"}, mock_context) + + assert result.result.data["conversation"] == SAMPLE_CONVERSATION + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONVERSATION) + + await freshdesk.execute_action("create_note", {"ticket_id": 5001, "body": "Note
"}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/tickets/5001/notes" in call_args.args[0] + assert call_args.kwargs["method"] == "POST" + + @pytest.mark.asyncio + async def test_private_true_in_body(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONVERSATION) + + await freshdesk.execute_action("create_note", {"ticket_id": 5001, "body": "Note
"}, mock_context) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["private"] is True + assert body["body"] == "Note
" + + @pytest.mark.asyncio + async def test_notify_emails_included_when_provided(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONVERSATION) + + await freshdesk.execute_action( + "create_note", + {"ticket_id": 5001, "body": "Note
", "notify_emails": ["agent@example.com"]}, + mock_context, + ) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["notify_emails"] == ["agent@example.com"] + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Create note failed") + + result = await freshdesk.execute_action("create_note", {"ticket_id": 5001, "body": "Note
"}, mock_context) + + assert result.type == ResultType.ACTION_ERROR + assert "Create note failed" in result.result.message + + +class TestCreateReply: + @pytest.mark.asyncio + async def test_happy_path_returns_conversation(self, mock_context): + reply = {**SAMPLE_CONVERSATION, "private": False} + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=reply) + + result = await freshdesk.execute_action( + "create_reply", {"ticket_id": 5001, "body": "Reply
"}, mock_context + ) + + assert result.result.data["conversation"]["private"] is False + + @pytest.mark.asyncio + async def test_request_url_and_method(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONVERSATION) + + await freshdesk.execute_action("create_reply", {"ticket_id": 5001, "body": "Reply
"}, mock_context) + + call_args = mock_context.fetch.call_args + assert "/tickets/5001/reply" in call_args.args[0] + assert call_args.kwargs["method"] == "POST" + + @pytest.mark.asyncio + async def test_body_in_request_payload(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONVERSATION) + + await freshdesk.execute_action("create_reply", {"ticket_id": 5001, "body": "Reply
"}, mock_context) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["body"] == "Reply
" + + @pytest.mark.asyncio + async def test_from_email_included_when_provided(self, mock_context): + mock_context.fetch.return_value = FetchResponse(status=201, headers={}, data=SAMPLE_CONVERSATION) + + await freshdesk.execute_action( + "create_reply", + {"ticket_id": 5001, "body": "Reply
", "from_email": "support@company.com"}, + mock_context, + ) + + body = mock_context.fetch.call_args.kwargs["json"] + assert body["from_email"] == "support@company.com" + + @pytest.mark.asyncio + async def test_exception_returns_action_error(self, mock_context): + mock_context.fetch.side_effect = Exception("Create reply failed") + + result = await freshdesk.execute_action( + "create_reply", {"ticket_id": 5001, "body": "Reply
"}, mock_context + ) + + assert result.type == ResultType.ACTION_ERROR + assert "Create reply failed" in result.result.message From a2cec797a0b975abf2e88858def374d10c3b8b4a Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Fri, 1 May 2026 14:47:19 +1200 Subject: [PATCH 2/4] fix(freshdesk): fix optional param access warnings and add integration tests - Replace all `inputs["param"]` with `inputs.get("param")` for optional parameters to prevent KeyError if the parameter is not provided - Add freshdesk/tests/test_freshdesk_integration.py with pytest.mark.integration tests that skip when FRESHDESK_API_KEY or FRESHDESK_DOMAIN env vars are missing --- freshdesk/freshdesk.py | 140 +++++++++--------- freshdesk/tests/test_freshdesk_integration.py | 66 +++++++++ 2 files changed, 136 insertions(+), 70 deletions(-) create mode 100644 freshdesk/tests/test_freshdesk_integration.py diff --git a/freshdesk/freshdesk.py b/freshdesk/freshdesk.py index a838be21..ca3abef7 100644 --- a/freshdesk/freshdesk.py +++ b/freshdesk/freshdesk.py @@ -97,17 +97,17 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): body = {"name": inputs["name"]} # Add optional fields if provided - if "description" in inputs and inputs["description"]: - body["description"] = inputs["description"] + if inputs.get("description"): + body["description"] = inputs.get("description") - if "domains" in inputs and inputs["domains"]: - body["domains"] = inputs["domains"] + if inputs.get("domains"): + body["domains"] = inputs.get("domains") - if "note" in inputs and inputs["note"]: - body["note"] = inputs["note"] + if inputs.get("note"): + body["note"] = inputs.get("note") - if "custom_fields" in inputs and inputs["custom_fields"]: - body["custom_fields"] = inputs["custom_fields"] + if inputs.get("custom_fields"): + body["custom_fields"] = inputs.get("custom_fields") # Get auth headers and base URL headers = get_auth_headers(context) @@ -163,20 +163,20 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): # Build request body with only provided fields body = {} - if "name" in inputs and inputs["name"]: - body["name"] = inputs["name"] + if inputs.get("name"): + body["name"] = inputs.get("name") - if "description" in inputs and inputs["description"]: - body["description"] = inputs["description"] + if inputs.get("description"): + body["description"] = inputs.get("description") - if "domains" in inputs and inputs["domains"]: - body["domains"] = inputs["domains"] + if inputs.get("domains"): + body["domains"] = inputs.get("domains") - if "note" in inputs and inputs["note"]: - body["note"] = inputs["note"] + if inputs.get("note"): + body["note"] = inputs.get("note") - if "custom_fields" in inputs and inputs["custom_fields"]: - body["custom_fields"] = inputs["custom_fields"] + if inputs.get("custom_fields"): + body["custom_fields"] = inputs.get("custom_fields") # Get auth headers and base URL headers = get_auth_headers(context) @@ -264,20 +264,20 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): try: body = {"subject": inputs["subject"], "email": inputs["email"]} - if "description" in inputs and inputs["description"]: - body["description"] = inputs["description"] - if "priority" in inputs: - body["priority"] = inputs["priority"] - if "status" in inputs: - body["status"] = inputs["status"] - if "source" in inputs: - body["source"] = inputs["source"] - if "name" in inputs and inputs["name"]: - body["name"] = inputs["name"] - if "company_id" in inputs: - body["company_id"] = inputs["company_id"] - if "tags" in inputs and inputs["tags"]: - body["tags"] = inputs["tags"] + if inputs.get("description"): + body["description"] = inputs.get("description") + if inputs.get("priority") is not None: + body["priority"] = inputs.get("priority") + if inputs.get("status") is not None: + body["status"] = inputs.get("status") + if inputs.get("source") is not None: + body["source"] = inputs.get("source") + if inputs.get("name"): + body["name"] = inputs.get("name") + if inputs.get("company_id") is not None: + body["company_id"] = inputs.get("company_id") + if inputs.get("tags"): + body["tags"] = inputs.get("tags") headers = get_auth_headers(context) base_url = get_base_url(context) @@ -339,16 +339,16 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): ticket_id = inputs["ticket_id"] body = {} - if "subject" in inputs and inputs["subject"]: - body["subject"] = inputs["subject"] - if "description" in inputs and inputs["description"]: - body["description"] = inputs["description"] - if "priority" in inputs: - body["priority"] = inputs["priority"] - if "status" in inputs: - body["status"] = inputs["status"] - if "tags" in inputs and inputs["tags"]: - body["tags"] = inputs["tags"] + if inputs.get("subject"): + body["subject"] = inputs.get("subject") + if inputs.get("description"): + body["description"] = inputs.get("description") + if inputs.get("priority") is not None: + body["priority"] = inputs.get("priority") + if inputs.get("status") is not None: + body["status"] = inputs.get("status") + if inputs.get("tags"): + body["tags"] = inputs.get("tags") headers = get_auth_headers(context) base_url = get_base_url(context) @@ -391,18 +391,18 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): try: body = {"name": inputs["name"], "email": inputs["email"]} - if "phone" in inputs and inputs["phone"]: - body["phone"] = inputs["phone"] - if "mobile" in inputs and inputs["mobile"]: - body["mobile"] = inputs["mobile"] - if "company_id" in inputs: - body["company_id"] = inputs["company_id"] - if "job_title" in inputs and inputs["job_title"]: - body["job_title"] = inputs["job_title"] - if "description" in inputs and inputs["description"]: - body["description"] = inputs["description"] - if "tags" in inputs and inputs["tags"]: - body["tags"] = inputs["tags"] + if inputs.get("phone"): + body["phone"] = inputs.get("phone") + if inputs.get("mobile"): + body["mobile"] = inputs.get("mobile") + if inputs.get("company_id") is not None: + body["company_id"] = inputs.get("company_id") + if inputs.get("job_title"): + body["job_title"] = inputs.get("job_title") + if inputs.get("description"): + body["description"] = inputs.get("description") + if inputs.get("tags"): + body["tags"] = inputs.get("tags") headers = get_auth_headers(context) base_url = get_base_url(context) @@ -464,18 +464,18 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): contact_id = inputs["contact_id"] body = {} - if "name" in inputs and inputs["name"]: - body["name"] = inputs["name"] - if "email" in inputs and inputs["email"]: - body["email"] = inputs["email"] - if "phone" in inputs and inputs["phone"]: - body["phone"] = inputs["phone"] - if "mobile" in inputs and inputs["mobile"]: - body["mobile"] = inputs["mobile"] - if "job_title" in inputs and inputs["job_title"]: - body["job_title"] = inputs["job_title"] - if "description" in inputs and inputs["description"]: - body["description"] = inputs["description"] + if inputs.get("name"): + body["name"] = inputs.get("name") + if inputs.get("email"): + body["email"] = inputs.get("email") + if inputs.get("phone"): + body["phone"] = inputs.get("phone") + if inputs.get("mobile"): + body["mobile"] = inputs.get("mobile") + if inputs.get("job_title"): + body["job_title"] = inputs.get("job_title") + if inputs.get("description"): + body["description"] = inputs.get("description") headers = get_auth_headers(context) base_url = get_base_url(context) @@ -578,8 +578,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): ticket_id = inputs["ticket_id"] body = {"body": inputs["body"], "private": True} - if "notify_emails" in inputs and inputs["notify_emails"]: - body["notify_emails"] = inputs["notify_emails"] + if inputs.get("notify_emails"): + body["notify_emails"] = inputs.get("notify_emails") headers = get_auth_headers(context) base_url = get_base_url(context) @@ -603,8 +603,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext): ticket_id = inputs["ticket_id"] body = {"body": inputs["body"]} - if "from_email" in inputs and inputs["from_email"]: - body["from_email"] = inputs["from_email"] + if inputs.get("from_email"): + body["from_email"] = inputs.get("from_email") headers = get_auth_headers(context) base_url = get_base_url(context) diff --git a/freshdesk/tests/test_freshdesk_integration.py b/freshdesk/tests/test_freshdesk_integration.py new file mode 100644 index 00000000..adb36076 --- /dev/null +++ b/freshdesk/tests/test_freshdesk_integration.py @@ -0,0 +1,66 @@ +import os +import sys +import importlib.util + +import pytest + +_parent = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +_deps = os.path.abspath(os.path.join(os.path.dirname(__file__), "../dependencies")) +sys.path.insert(0, _parent) +sys.path.insert(0, _deps) + +from autohive_integrations_sdk import ExecutionContext # noqa: E402 + +_spec = importlib.util.spec_from_file_location("freshdesk_mod", os.path.join(_parent, "freshdesk.py")) +_mod = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_mod) + +freshdesk = _mod.freshdesk # the Integration instance + +pytestmark = pytest.mark.integration + +# Skip all integration tests if env vars are not set +API_KEY = os.environ.get("FRESHDESK_API_KEY", "") +DOMAIN = os.environ.get("FRESHDESK_DOMAIN", "") + +skip_if_no_creds = pytest.mark.skipif( + not API_KEY or not DOMAIN, + reason="FRESHDESK_API_KEY and FRESHDESK_DOMAIN env vars required for integration tests", +) + + +@pytest.fixture +def live_context(): + """Real ExecutionContext using env var credentials.""" + auth = {"credentials": {"api_key": API_KEY, "domain": DOMAIN}} # nosec B105 + return auth + + +@skip_if_no_creds +@pytest.mark.asyncio +async def test_list_companies_integration(live_context): + """Integration test: list companies from live Freshdesk account.""" + async with ExecutionContext(auth=live_context) as context: + result = await freshdesk.execute_action("list_companies", {"per_page": 5}, context) + assert result.result is not None + assert "companies" in result.result.data + + +@skip_if_no_creds +@pytest.mark.asyncio +async def test_list_tickets_integration(live_context): + """Integration test: list tickets from live Freshdesk account.""" + async with ExecutionContext(auth=live_context) as context: + result = await freshdesk.execute_action("list_tickets", {"per_page": 5}, context) + assert result.result is not None + assert "tickets" in result.result.data + + +@skip_if_no_creds +@pytest.mark.asyncio +async def test_list_contacts_integration(live_context): + """Integration test: list contacts from live Freshdesk account.""" + async with ExecutionContext(auth=live_context) as context: + result = await freshdesk.execute_action("list_contacts", {"per_page": 5}, context) + assert result.result is not None + assert "contacts" in result.result.data From 9b5914bdcafa8968761b3437d20dfbe0f3043eea Mon Sep 17 00:00:00 2001 From: Kai KoenigThis is a test ticket created via API integration.
", - "email": "customer@example.com", - "priority": 2, - "status": 2, - "tags": ["api-test"], - } - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("create_ticket", inputs, context) - print(f"Create Ticket Result: {result}") - return result - except Exception as e: - print(f"Error testing create_ticket: {e}") - return None - - -async def test_list_tickets(): - """Test listing all tickets.""" - auth = {"credentials": {"api_key": "your_api_key_here", "domain": "your_domain_here"}} - - inputs = {"per_page": 10} - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("list_tickets", inputs, context) - print(f"List Tickets Result: {result}") - return result - except Exception as e: - print(f"Error testing list_tickets: {e}") - return None - - -async def test_get_ticket(): - """Test getting a specific ticket.""" - auth = {"credentials": {"api_key": "your_api_key_here", "domain": "your_domain_here"}} - - inputs = {"ticket_id": 1} # Replace with actual ticket ID - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("get_ticket", inputs, context) - print(f"Get Ticket Result: {result}") - return result - except Exception as e: - print(f"Error testing get_ticket: {e}") - return None - - -async def test_create_contact(): - """Test creating a new contact.""" - auth = {"credentials": {"api_key": "your_api_key_here", "domain": "your_domain_here"}} - - inputs = { - "name": "John Doe", - "email": "john.doe@example.com", - "phone": "555-0123", - "job_title": "Software Engineer", - } - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("create_contact", inputs, context) - print(f"Create Contact Result: {result}") - return result - except Exception as e: - print(f"Error testing create_contact: {e}") - return None - - -async def test_list_contacts(): - """Test listing all contacts.""" - auth = {"credentials": {"api_key": "your_api_key_here", "domain": "your_domain_here"}} - - inputs = {"per_page": 10} - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("list_contacts", inputs, context) - print(f"List Contacts Result: {result}") - return result - except Exception as e: - print(f"Error testing list_contacts: {e}") - return None - - -async def test_search_contacts(): - """Test searching for contacts by name.""" - auth = {"credentials": {"api_key": "your_api_key_here", "domain": "your_domain_here"}} - - inputs = {"term": "John"} # Replace with a contact name that exists in your account - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("search_contacts", inputs, context) - print(f"Search Contacts Result: {result}") - return result - except Exception as e: - print(f"Error testing search_contacts: {e}") - return None - - -async def test_list_conversations(): - """Test listing conversations for a ticket.""" - auth = {"credentials": {"api_key": "your_api_key_here", "domain": "your_domain_here"}} - - inputs = {"ticket_id": 1} # Replace with actual ticket ID - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("list_conversations", inputs, context) - print(f"List Conversations Result: {result}") - return result - except Exception as e: - print(f"Error testing list_conversations: {e}") - return None - - -async def test_create_note(): - """Test creating a private note on a ticket.""" - auth = {"credentials": {"api_key": "your_api_key_here", "domain": "your_domain_here"}} - - inputs = { - "ticket_id": 1, # Replace with actual ticket ID - "body": "This is a private note for internal use.
", - } - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("create_note", inputs, context) - print(f"Create Note Result: {result}") - return result - except Exception as e: - print(f"Error testing create_note: {e}") - return None - - -async def test_create_reply(): - """Test creating a public reply on a ticket.""" - auth = {"credentials": {"api_key": "your_api_key_here", "domain": "your_domain_here"}} - - inputs = { - "ticket_id": 1, # Replace with actual ticket ID - "body": "This is a public reply to the customer.
", - } - - async with ExecutionContext(auth=auth) as context: - try: - result = await freshdesk.execute_action("create_reply", inputs, context) - print(f"Create Reply Result: {result}") - return result - except Exception as e: - print(f"Error testing create_reply: {e}") - return None - - -async def main(): - print("Testing Freshdesk Integration") - print("=" * 60) - print() - print("NOTE: Replace 'your_api_key_here' and 'your_domain_here'") - print(" with actual credentials before running tests.") - print() - print("=" * 60) - print() - - # Test company actions - print("1. Testing list_companies...") - await test_list_companies() - print() - - print("2. Testing search_companies...") - await test_search_companies() - print() - - print("3. Testing create_company...") - await test_create_company() - print() - - print("4. Testing get_company...") - await test_get_company() - print() - - # Test ticket actions - print("5. Testing create_ticket...") - await test_create_ticket() - print() - - print("6. Testing list_tickets...") - await test_list_tickets() - print() - - print("7. Testing get_ticket...") - await test_get_ticket() - print() - - # Test contact actions - print("8. Testing create_contact...") - await test_create_contact() - print() - - print("9. Testing list_contacts...") - await test_list_contacts() - print() - - print("10. Testing search_contacts...") - await test_search_contacts() - print() - - # Test conversation actions - print("11. Testing list_conversations...") - await test_list_conversations() - print() - - print("12. Testing create_note...") - await test_create_note() - print() - - print("13. Testing create_reply...") - await test_create_reply() - print() - - print("=" * 60) - print("Testing completed!") - print("=" * 60) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/freshdesk/tests/test_freshdesk_integration.py b/freshdesk/tests/test_freshdesk_integration.py index adb36076..67f70ee7 100644 --- a/freshdesk/tests/test_freshdesk_integration.py +++ b/freshdesk/tests/test_freshdesk_integration.py @@ -1,66 +1,159 @@ -import os -import sys -import importlib.util +""" +Live integration tests for the Freshdesk integration. -import pytest +Requires FRESHDESK_API_KEY and FRESHDESK_DOMAIN set in the environment or +project .env. -_parent = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -_deps = os.path.abspath(os.path.join(os.path.dirname(__file__), "../dependencies")) -sys.path.insert(0, _parent) -sys.path.insert(0, _deps) +Credential extraction recipe: +1. In Freshdesk, open Profile Settings > View API key and copy the API key to + FRESHDESK_API_KEY. +2. Copy the Freshdesk subdomain to FRESHDESK_DOMAIN. For example, use + "acme" for https://acme.freshdesk.com. +3. Add both values to the project .env file or export them in your shell before + running these tests. -from autohive_integrations_sdk import ExecutionContext # noqa: E402 +Safe read-only run: + pytest freshdesk/tests/test_freshdesk_integration.py -m "integration and not destructive" +""" -_spec = importlib.util.spec_from_file_location("freshdesk_mod", os.path.join(_parent, "freshdesk.py")) -_mod = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(_mod) +import aiohttp +import pytest +from autohive_integrations_sdk import FetchResponse, ResultType -freshdesk = _mod.freshdesk # the Integration instance +from freshdesk import freshdesk pytestmark = pytest.mark.integration -# Skip all integration tests if env vars are not set -API_KEY = os.environ.get("FRESHDESK_API_KEY", "") -DOMAIN = os.environ.get("FRESHDESK_DOMAIN", "") -skip_if_no_creds = pytest.mark.skipif( - not API_KEY or not DOMAIN, - reason="FRESHDESK_API_KEY and FRESHDESK_DOMAIN env vars required for integration tests", -) +@pytest.fixture +def live_context(env_credentials, make_context): + api_key = env_credentials("FRESHDESK_API_KEY") + domain = env_credentials("FRESHDESK_DOMAIN") + if not api_key: + pytest.skip("FRESHDESK_API_KEY not set — skipping integration tests") + if not domain: + pytest.skip("FRESHDESK_DOMAIN not set — skipping integration tests") + async def real_fetch(url, *, method="GET", json=None, headers=None, params=None, **kwargs): + async with aiohttp.ClientSession() as session: + async with session.request( + method, + url, + json=json, + headers=headers, + params=params, + **kwargs, + ) as resp: + try: + data = await resp.json(content_type=None) + except Exception: + data = await resp.text() + return FetchResponse(status=resp.status, headers=dict(resp.headers), data=data) -@pytest.fixture -def live_context(): - """Real ExecutionContext using env var credentials.""" - auth = {"credentials": {"api_key": API_KEY, "domain": DOMAIN}} # nosec B105 - return auth - - -@skip_if_no_creds -@pytest.mark.asyncio -async def test_list_companies_integration(live_context): - """Integration test: list companies from live Freshdesk account.""" - async with ExecutionContext(auth=live_context) as context: - result = await freshdesk.execute_action("list_companies", {"per_page": 5}, context) - assert result.result is not None - assert "companies" in result.result.data - - -@skip_if_no_creds -@pytest.mark.asyncio -async def test_list_tickets_integration(live_context): - """Integration test: list tickets from live Freshdesk account.""" - async with ExecutionContext(auth=live_context) as context: - result = await freshdesk.execute_action("list_tickets", {"per_page": 5}, context) - assert result.result is not None - assert "tickets" in result.result.data - - -@skip_if_no_creds -@pytest.mark.asyncio -async def test_list_contacts_integration(live_context): - """Integration test: list contacts from live Freshdesk account.""" - async with ExecutionContext(auth=live_context) as context: - result = await freshdesk.execute_action("list_contacts", {"per_page": 5}, context) - assert result.result is not None - assert "contacts" in result.result.data + ctx = make_context(auth={"api_key": api_key, "domain": domain}) + ctx.fetch.side_effect = real_fetch + return ctx + + +async def _first_ticket_id(live_context): + result = await freshdesk.execute_action("list_tickets", {"per_page": 5}, live_context) + if result.type != ResultType.ACTION: + pytest.skip(f"Unable to list Freshdesk tickets: {result.result.message}") + + tickets = result.result.data["tickets"] + if not tickets: + pytest.skip("No Freshdesk tickets available for ticket-scoped live tests") + return tickets[0]["id"] + + +async def _first_company_id(live_context): + result = await freshdesk.execute_action("list_companies", {"per_page": 5}, live_context) + if result.type != ResultType.ACTION: + pytest.skip(f"Unable to list Freshdesk companies: {result.result.message}") + + companies = result.result.data["companies"] + if not companies: + pytest.skip("No Freshdesk companies available for company-scoped live tests") + return companies[0]["id"] + + +async def _first_contact_id(live_context): + result = await freshdesk.execute_action("list_contacts", {"per_page": 5}, live_context) + if result.type != ResultType.ACTION: + pytest.skip(f"Unable to list Freshdesk contacts: {result.result.message}") + + contacts = result.result.data["contacts"] + if not contacts: + pytest.skip("No Freshdesk contacts available for contact-scoped live tests") + return contacts[0]["id"] + + +async def test_list_companies_returns_companies(live_context): + result = await freshdesk.execute_action("list_companies", {"per_page": 5}, live_context) + + assert result.type == ResultType.ACTION + data = result.result.data + assert "companies" in data + assert isinstance(data["companies"], list) + + +async def test_list_tickets_returns_tickets(live_context): + result = await freshdesk.execute_action("list_tickets", {"per_page": 5}, live_context) + + assert result.type == ResultType.ACTION + data = result.result.data + assert "tickets" in data + assert isinstance(data["tickets"], list) + + +async def test_list_contacts_returns_contacts(live_context): + result = await freshdesk.execute_action("list_contacts", {"per_page": 5}, live_context) + + assert result.type == ResultType.ACTION + data = result.result.data + assert "contacts" in data + assert isinstance(data["contacts"], list) + + +async def test_get_company_returns_company_shape(live_context): + company_id = await _first_company_id(live_context) + + result = await freshdesk.execute_action("get_company", {"company_id": company_id}, live_context) + + assert result.type == ResultType.ACTION + data = result.result.data + assert "company" in data + assert data["company"]["id"] == company_id + + +async def test_get_ticket_returns_ticket_shape(live_context): + ticket_id = await _first_ticket_id(live_context) + + result = await freshdesk.execute_action("get_ticket", {"ticket_id": ticket_id}, live_context) + + assert result.type == ResultType.ACTION + data = result.result.data + assert "ticket" in data + assert data["ticket"]["id"] == ticket_id + + +async def test_get_contact_returns_contact_shape(live_context): + contact_id = await _first_contact_id(live_context) + + result = await freshdesk.execute_action("get_contact", {"contact_id": contact_id}, live_context) + + assert result.type == ResultType.ACTION + data = result.result.data + assert "contact" in data + assert data["contact"]["id"] == contact_id + + +async def test_list_conversations_returns_conversations(live_context): + ticket_id = await _first_ticket_id(live_context) + + result = await freshdesk.execute_action("list_conversations", {"ticket_id": ticket_id}, live_context) + + assert result.type == ResultType.ACTION + data = result.result.data + assert "conversations" in data + assert isinstance(data["conversations"], list) From 90496d3e3f6b83b4ec112f5346cbfdc44bd32f96 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Tue, 12 May 2026 14:30:04 +1200 Subject: [PATCH 4/4] test(freshdesk): add e2e coverage for all 20 actions, fix note field nullability - Fixed import in test file (module vs Integration instance) - Expanded integration tests from 7 to 8 covering all 20 actions - Added company lifecycle (create, get, update, delete) - Added contact lifecycle (create, get, update, delete) - Added ticket lifecycle (create, get, update, list_conversations, create_note, create_reply, delete) - Added search_companies and search_contacts read-only tests - Fixed config.json: note field type string -> ["string", "null"] to match Freshdesk API returning null --- freshdesk/config.json | 226 +++++++++++---- freshdesk/tests/test_freshdesk_integration.py | 267 +++++++++++------- 2 files changed, 338 insertions(+), 155 deletions(-) diff --git a/freshdesk/config.json b/freshdesk/config.json index a89aa0c7..9c29be91 100644 --- a/freshdesk/config.json +++ b/freshdesk/config.json @@ -74,7 +74,10 @@ } }, "note": { - "type": "string", + "type": [ + "string", + "null" + ], "description": "Additional notes about the company" }, "created_at": { @@ -93,7 +96,10 @@ "description": "Total number of companies" } }, - "required": ["companies", "total"] + "required": [ + "companies", + "total" + ] } }, "create_company": { @@ -118,7 +124,10 @@ } }, "note": { - "type": "string", + "type": [ + "string", + "null" + ], "description": "Additional notes about the company" }, "custom_fields": { @@ -126,7 +135,9 @@ "description": "Custom fields for the company (key-value pairs)" } }, - "required": ["name"] + "required": [ + "name" + ] }, "output_schema": { "type": "object", @@ -155,7 +166,10 @@ } }, "note": { - "type": "string", + "type": [ + "string", + "null" + ], "description": "Additional notes about the company" }, "created_at": { @@ -169,7 +183,9 @@ } } }, - "required": ["company"] + "required": [ + "company" + ] } }, "get_company": { @@ -183,7 +199,9 @@ "description": "The unique ID of the company to retrieve" } }, - "required": ["company_id"] + "required": [ + "company_id" + ] }, "output_schema": { "type": "object", @@ -212,7 +230,10 @@ } }, "note": { - "type": "string", + "type": [ + "string", + "null" + ], "description": "Additional notes about the company" }, "created_at": { @@ -226,7 +247,9 @@ } } }, - "required": ["company"] + "required": [ + "company" + ] } }, "update_company": { @@ -255,7 +278,10 @@ } }, "note": { - "type": "string", + "type": [ + "string", + "null" + ], "description": "Updated notes about the company" }, "custom_fields": { @@ -263,7 +289,9 @@ "description": "Updated custom fields (key-value pairs)" } }, - "required": ["company_id"] + "required": [ + "company_id" + ] }, "output_schema": { "type": "object", @@ -292,7 +320,10 @@ } }, "note": { - "type": "string", + "type": [ + "string", + "null" + ], "description": "Additional notes about the company" }, "created_at": { @@ -306,7 +337,9 @@ } } }, - "required": ["company"] + "required": [ + "company" + ] } }, "delete_company": { @@ -320,7 +353,9 @@ "description": "The unique ID of the company to delete" } }, - "required": ["company_id"] + "required": [ + "company_id" + ] }, "output_schema": { "type": "object", @@ -330,7 +365,9 @@ "description": "Whether the company was deleted" } }, - "required": ["deleted"] + "required": [ + "deleted" + ] } }, "search_companies": { @@ -344,7 +381,9 @@ "description": "Company name or keyword to search for (case insensitive, requires word prefix matching)" } }, - "required": ["name"] + "required": [ + "name" + ] }, "output_schema": { "type": "object", @@ -371,7 +410,10 @@ "description": "Total number of companies found" } }, - "required": ["companies", "total"] + "required": [ + "companies", + "total" + ] } }, "create_ticket": { @@ -395,17 +437,34 @@ "priority": { "type": "integer", "description": "Priority: 1-Low, 2-Medium, 3-High, 4-Urgent", - "enum": [1, 2, 3, 4] + "enum": [ + 1, + 2, + 3, + 4 + ] }, "status": { "type": "integer", "description": "Status: 2-Open, 3-Pending, 4-Resolved, 5-Closed", - "enum": [2, 3, 4, 5] + "enum": [ + 2, + 3, + 4, + 5 + ] }, "source": { "type": "integer", "description": "Source: 1-Email, 2-Portal, 3-Phone, 7-Chat, 9-Feedback, 10-Outbound Email", - "enum": [1, 2, 3, 7, 9, 10] + "enum": [ + 1, + 2, + 3, + 7, + 9, + 10 + ] }, "name": { "type": "string", @@ -423,7 +482,10 @@ } } }, - "required": ["subject", "email"] + "required": [ + "subject", + "email" + ] }, "output_schema": { "type": "object", @@ -433,7 +495,9 @@ "description": "Created ticket details" } }, - "required": ["ticket"] + "required": [ + "ticket" + ] } }, "list_tickets": { @@ -468,7 +532,10 @@ "description": "Total number of tickets returned" } }, - "required": ["tickets", "total"] + "required": [ + "tickets", + "total" + ] } }, "get_ticket": { @@ -482,7 +549,9 @@ "description": "The unique ID of the ticket to retrieve" } }, - "required": ["ticket_id"] + "required": [ + "ticket_id" + ] }, "output_schema": { "type": "object", @@ -492,7 +561,9 @@ "description": "Ticket details" } }, - "required": ["ticket"] + "required": [ + "ticket" + ] } }, "update_ticket": { @@ -516,12 +587,22 @@ "priority": { "type": "integer", "description": "Priority: 1-Low, 2-Medium, 3-High, 4-Urgent", - "enum": [1, 2, 3, 4] + "enum": [ + 1, + 2, + 3, + 4 + ] }, "status": { "type": "integer", "description": "Status: 2-Open, 3-Pending, 4-Resolved, 5-Closed", - "enum": [2, 3, 4, 5] + "enum": [ + 2, + 3, + 4, + 5 + ] }, "tags": { "type": "array", @@ -531,7 +612,9 @@ } } }, - "required": ["ticket_id"] + "required": [ + "ticket_id" + ] }, "output_schema": { "type": "object", @@ -541,7 +624,9 @@ "description": "Updated ticket details" } }, - "required": ["ticket"] + "required": [ + "ticket" + ] } }, "delete_ticket": { @@ -555,7 +640,9 @@ "description": "The unique ID of the ticket to delete" } }, - "required": ["ticket_id"] + "required": [ + "ticket_id" + ] }, "output_schema": { "type": "object", @@ -565,7 +652,9 @@ "description": "Whether the ticket was deleted" } }, - "required": ["deleted"] + "required": [ + "deleted" + ] } }, "create_contact": { @@ -610,7 +699,10 @@ } } }, - "required": ["name", "email"] + "required": [ + "name", + "email" + ] }, "output_schema": { "type": "object", @@ -620,7 +712,9 @@ "description": "Created contact details" } }, - "required": ["contact"] + "required": [ + "contact" + ] } }, "list_contacts": { @@ -655,7 +749,10 @@ "description": "Total number of contacts returned" } }, - "required": ["contacts", "total"] + "required": [ + "contacts", + "total" + ] } }, "get_contact": { @@ -669,7 +766,9 @@ "description": "The unique ID of the contact to retrieve" } }, - "required": ["contact_id"] + "required": [ + "contact_id" + ] }, "output_schema": { "type": "object", @@ -679,7 +778,9 @@ "description": "Contact details" } }, - "required": ["contact"] + "required": [ + "contact" + ] } }, "update_contact": { @@ -717,7 +818,9 @@ "description": "Updated description" } }, - "required": ["contact_id"] + "required": [ + "contact_id" + ] }, "output_schema": { "type": "object", @@ -727,7 +830,9 @@ "description": "Updated contact details" } }, - "required": ["contact"] + "required": [ + "contact" + ] } }, "delete_contact": { @@ -741,7 +846,9 @@ "description": "The unique ID of the contact to delete" } }, - "required": ["contact_id"] + "required": [ + "contact_id" + ] }, "output_schema": { "type": "object", @@ -751,7 +858,9 @@ "description": "Whether the contact was deleted" } }, - "required": ["deleted"] + "required": [ + "deleted" + ] } }, "search_contacts": { @@ -765,7 +874,9 @@ "description": "Contact name or keyword to search for (case insensitive, requires word prefix matching)" } }, - "required": ["term"] + "required": [ + "term" + ] }, "output_schema": { "type": "object", @@ -792,7 +903,10 @@ "description": "Total number of contacts found" } }, - "required": ["contacts", "total"] + "required": [ + "contacts", + "total" + ] } }, "list_conversations": { @@ -806,7 +920,9 @@ "description": "The unique ID of the ticket" } }, - "required": ["ticket_id"] + "required": [ + "ticket_id" + ] }, "output_schema": { "type": "object", @@ -816,7 +932,9 @@ "description": "List of conversations" } }, - "required": ["conversations"] + "required": [ + "conversations" + ] } }, "create_note": { @@ -841,7 +959,10 @@ } } }, - "required": ["ticket_id", "body"] + "required": [ + "ticket_id", + "body" + ] }, "output_schema": { "type": "object", @@ -851,7 +972,9 @@ "description": "Created note details" } }, - "required": ["conversation"] + "required": [ + "conversation" + ] } }, "create_reply": { @@ -873,7 +996,10 @@ "description": "Email address from which the reply is sent" } }, - "required": ["ticket_id", "body"] + "required": [ + "ticket_id", + "body" + ] }, "output_schema": { "type": "object", @@ -883,8 +1009,10 @@ "description": "Created reply details" } }, - "required": ["conversation"] + "required": [ + "conversation" + ] } } } -} +} \ No newline at end of file diff --git a/freshdesk/tests/test_freshdesk_integration.py b/freshdesk/tests/test_freshdesk_integration.py index 67f70ee7..b5bf24ef 100644 --- a/freshdesk/tests/test_freshdesk_integration.py +++ b/freshdesk/tests/test_freshdesk_integration.py @@ -1,26 +1,19 @@ """ Live integration tests for the Freshdesk integration. -Requires FRESHDESK_API_KEY and FRESHDESK_DOMAIN set in the environment or -project .env. - -Credential extraction recipe: -1. In Freshdesk, open Profile Settings > View API key and copy the API key to - FRESHDESK_API_KEY. -2. Copy the Freshdesk subdomain to FRESHDESK_DOMAIN. For example, use - "acme" for https://acme.freshdesk.com. -3. Add both values to the project .env file or export them in your shell before - running these tests. - -Safe read-only run: - pytest freshdesk/tests/test_freshdesk_integration.py -m "integration and not destructive" +Requires FRESHDESK_API_KEY and FRESHDESK_DOMAIN set in the environment. + +Run with: + pytest freshdesk/tests/test_freshdesk_integration.py -m "integration" --import-mode=importlib --tb=short """ +import time + import aiohttp import pytest from autohive_integrations_sdk import FetchResponse, ResultType -from freshdesk import freshdesk +from freshdesk.freshdesk import freshdesk pytestmark = pytest.mark.integration @@ -36,14 +29,7 @@ def live_context(env_credentials, make_context): async def real_fetch(url, *, method="GET", json=None, headers=None, params=None, **kwargs): async with aiohttp.ClientSession() as session: - async with session.request( - method, - url, - json=json, - headers=headers, - params=params, - **kwargs, - ) as resp: + async with session.request(method, url, json=json, headers=headers, params=params, **kwargs) as resp: try: data = await resp.json(content_type=None) except Exception: @@ -55,105 +41,174 @@ async def real_fetch(url, *, method="GET", json=None, headers=None, params=None, return ctx -async def _first_ticket_id(live_context): - result = await freshdesk.execute_action("list_tickets", {"per_page": 5}, live_context) - if result.type != ResultType.ACTION: - pytest.skip(f"Unable to list Freshdesk tickets: {result.result.message}") - - tickets = result.result.data["tickets"] - if not tickets: - pytest.skip("No Freshdesk tickets available for ticket-scoped live tests") - return tickets[0]["id"] +# ---- Read-Only Tests ---- -async def _first_company_id(live_context): +async def test_list_companies(live_context): result = await freshdesk.execute_action("list_companies", {"per_page": 5}, live_context) - if result.type != ResultType.ACTION: - pytest.skip(f"Unable to list Freshdesk companies: {result.result.message}") - - companies = result.result.data["companies"] - if not companies: - pytest.skip("No Freshdesk companies available for company-scoped live tests") - return companies[0]["id"] - - -async def _first_contact_id(live_context): - result = await freshdesk.execute_action("list_contacts", {"per_page": 5}, live_context) - if result.type != ResultType.ACTION: - pytest.skip(f"Unable to list Freshdesk contacts: {result.result.message}") - - contacts = result.result.data["contacts"] - if not contacts: - pytest.skip("No Freshdesk contacts available for contact-scoped live tests") - return contacts[0]["id"] - - -async def test_list_companies_returns_companies(live_context): - result = await freshdesk.execute_action("list_companies", {"per_page": 5}, live_context) - assert result.type == ResultType.ACTION - data = result.result.data - assert "companies" in data - assert isinstance(data["companies"], list) + assert "companies" in result.result.data + assert isinstance(result.result.data["companies"], list) -async def test_list_tickets_returns_tickets(live_context): +async def test_list_tickets(live_context): result = await freshdesk.execute_action("list_tickets", {"per_page": 5}, live_context) - assert result.type == ResultType.ACTION - data = result.result.data - assert "tickets" in data - assert isinstance(data["tickets"], list) + assert "tickets" in result.result.data + assert isinstance(result.result.data["tickets"], list) -async def test_list_contacts_returns_contacts(live_context): +async def test_list_contacts(live_context): result = await freshdesk.execute_action("list_contacts", {"per_page": 5}, live_context) - assert result.type == ResultType.ACTION - data = result.result.data - assert "contacts" in data - assert isinstance(data["contacts"], list) + assert "contacts" in result.result.data + assert isinstance(result.result.data["contacts"], list) -async def test_get_company_returns_company_shape(live_context): - company_id = await _first_company_id(live_context) - - result = await freshdesk.execute_action("get_company", {"company_id": company_id}, live_context) - - assert result.type == ResultType.ACTION - data = result.result.data - assert "company" in data - assert data["company"]["id"] == company_id - - -async def test_get_ticket_returns_ticket_shape(live_context): - ticket_id = await _first_ticket_id(live_context) - - result = await freshdesk.execute_action("get_ticket", {"ticket_id": ticket_id}, live_context) - +async def test_search_companies(live_context): + result = await freshdesk.execute_action("search_companies", {"name": "a"}, live_context) assert result.type == ResultType.ACTION - data = result.result.data - assert "ticket" in data - assert data["ticket"]["id"] == ticket_id - - -async def test_get_contact_returns_contact_shape(live_context): - contact_id = await _first_contact_id(live_context) - - result = await freshdesk.execute_action("get_contact", {"contact_id": contact_id}, live_context) - - assert result.type == ResultType.ACTION - data = result.result.data - assert "contact" in data - assert data["contact"]["id"] == contact_id - - -async def test_list_conversations_returns_conversations(live_context): - ticket_id = await _first_ticket_id(live_context) + assert "companies" in result.result.data - result = await freshdesk.execute_action("list_conversations", {"ticket_id": ticket_id}, live_context) +async def test_search_contacts(live_context): + result = await freshdesk.execute_action("search_contacts", {"term": "a"}, live_context) assert result.type == ResultType.ACTION - data = result.result.data - assert "conversations" in data - assert isinstance(data["conversations"], list) + assert "contacts" in result.result.data + + +# ---- Destructive / Lifecycle Tests ---- + + +@pytest.mark.destructive +async def test_company_lifecycle(live_context): + """create -> get -> update -> delete company.""" + uid = int(time.time()) + + # Create + create = await freshdesk.execute_action( + "create_company", + {"name": f"AH Test Co {uid}", "description": "Created by integration test"}, + live_context, + ) + assert create.type == ResultType.ACTION + company_id = create.result.data["company"]["id"] + assert company_id + + # Get + get = await freshdesk.execute_action("get_company", {"company_id": company_id}, live_context) + assert get.type == ResultType.ACTION + assert get.result.data["company"]["id"] == company_id + + # Update + update = await freshdesk.execute_action( + "update_company", + {"company_id": company_id, "description": f"Updated at {uid}"}, + live_context, + ) + assert update.type == ResultType.ACTION + assert update.result.data["company"]["id"] == company_id + + # Delete + delete = await freshdesk.execute_action("delete_company", {"company_id": company_id}, live_context) + assert delete.type == ResultType.ACTION + assert delete.result.data["deleted"] is True + + +@pytest.mark.destructive +async def test_contact_lifecycle(live_context): + """create -> get -> update -> delete contact.""" + uid = int(time.time()) + + # Create + create = await freshdesk.execute_action( + "create_contact", + {"name": f"AH Test Contact {uid}", "email": f"ah-test-{uid}@example.com"}, + live_context, + ) + assert create.type == ResultType.ACTION + contact_id = create.result.data["contact"]["id"] + assert contact_id + + # Get + get = await freshdesk.execute_action("get_contact", {"contact_id": contact_id}, live_context) + assert get.type == ResultType.ACTION + assert get.result.data["contact"]["id"] == contact_id + + # Update + update = await freshdesk.execute_action( + "update_contact", + {"contact_id": contact_id, "job_title": "Test Engineer"}, + live_context, + ) + assert update.type == ResultType.ACTION + assert update.result.data["contact"]["id"] == contact_id + + # Delete (soft delete) + delete = await freshdesk.execute_action("delete_contact", {"contact_id": contact_id}, live_context) + assert delete.type == ResultType.ACTION + assert delete.result.data["deleted"] is True + + +@pytest.mark.destructive +async def test_ticket_lifecycle(live_context): + """create -> get -> update -> list_conversations -> create_note -> create_reply -> delete ticket.""" + uid = int(time.time()) + + # Create + create = await freshdesk.execute_action( + "create_ticket", + { + "subject": f"AH Test Ticket {uid}", + "email": f"ah-test-{uid}@example.com", + "description": "Integration test ticket", + "priority": 1, + "status": 2, + }, + live_context, + ) + assert create.type == ResultType.ACTION + ticket_id = create.result.data["ticket"]["id"] + assert ticket_id + + # Get + get = await freshdesk.execute_action("get_ticket", {"ticket_id": ticket_id}, live_context) + assert get.type == ResultType.ACTION + assert get.result.data["ticket"]["id"] == ticket_id + + # Update + update = await freshdesk.execute_action( + "update_ticket", + {"ticket_id": ticket_id, "priority": 2, "subject": f"AH Updated Ticket {uid}"}, + live_context, + ) + assert update.type == ResultType.ACTION + assert update.result.data["ticket"]["id"] == ticket_id + + # List conversations + convs = await freshdesk.execute_action("list_conversations", {"ticket_id": ticket_id}, live_context) + assert convs.type == ResultType.ACTION + assert "conversations" in convs.result.data + + # Create note + note = await freshdesk.execute_action( + "create_note", + {"ticket_id": ticket_id, "body": f"Test note at {uid}"}, + live_context, + ) + assert note.type == ResultType.ACTION + assert "conversation" in note.result.data + + # Create reply + reply = await freshdesk.execute_action( + "create_reply", + {"ticket_id": ticket_id, "body": f"Test reply at {uid}"}, + live_context, + ) + assert reply.type == ResultType.ACTION + assert "conversation" in reply.result.data + + # Delete + delete = await freshdesk.execute_action("delete_ticket", {"ticket_id": ticket_id}, live_context) + assert delete.type == ResultType.ACTION + assert delete.result.data["deleted"] is True