diff --git a/toolkits/jira/arcade_jira/client.py b/toolkits/jira/arcade_jira/client.py index 4661cf3e..03475d2f 100644 --- a/toolkits/jira/arcade_jira/client.py +++ b/toolkits/jira/arcade_jira/client.py @@ -56,26 +56,29 @@ class JiraClient: headers={"Authorization": f"Bearer {self.auth_token}"}, ) - data = response.json() + available_resources = deduplicate_available_resources(response.json()) - if len(data) == 0: + if len(available_resources) == 0: raise JiraToolExecutionError( message="No cloud ID returned by Atlassian, cannot make API calls" ) - if len(data) > 1: + if len(available_resources) > 1: cloud_ids_found = json.dumps([ { - "id": item["id"], - "name": item["name"], - "url": item["url"], + "id": resource["id"], + "name": resource["name"], + "url": resource["url"], } - for item in data + for resource in available_resources ]) raise JiraToolExecutionError( - message=f"Multiple cloud IDs returned by Atlassian: {cloud_ids_found}. " - "Cannot resolve which one to use." + message=( + "Multiple cloud IDs returned by Atlassian, cannot resolve which one " + "to use. Please revoke your authorization access and authorize a single " + f"Atlassian Cloud. Available cloud IDs: {cloud_ids_found}. " + ) ) - return cast(dict[str, Any], data[0]) + return cast(dict[str, Any], available_resources[0]) def _build_error_messages(self, response: httpx.Response) -> tuple[str, str | None]: try: @@ -224,3 +227,15 @@ class JiraClient: self._raise_for_status(response) return self._format_response_dict(response) + + +def deduplicate_available_resources(available_resources: list[dict]) -> list[dict]: + account_ids_seen = set() + deduplicated = [] + + for item in available_resources: + if item["id"] not in account_ids_seen: + deduplicated.append(item) + account_ids_seen.add(item["id"]) + + return deduplicated diff --git a/toolkits/jira/pyproject.toml b/toolkits/jira/pyproject.toml index bfd198b9..3e7a4e30 100644 --- a/toolkits/jira/pyproject.toml +++ b/toolkits/jira/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "arcade_jira" -version = "0.1.2" +version = "0.1.3" description = "Arcade.dev LLM tools for interacting with Atlassian Jira" requires-python = ">=3.10" dependencies = [ diff --git a/toolkits/jira/tests/test_client.py b/toolkits/jira/tests/test_client.py new file mode 100644 index 00000000..19cb2e9d --- /dev/null +++ b/toolkits/jira/tests/test_client.py @@ -0,0 +1,63 @@ +import json + +import httpx +import pytest + +from arcade_jira.client import JiraClient +from arcade_jira.exceptions import JiraToolExecutionError + + +@pytest.mark.asyncio +async def test_get_cloud_data_from_available_resources_single_cloud( + mock_httpx_client, fake_auth_token +): + cloud = {"id": "123", "name": "Test Cloud", "url": "https://test.atlassian.net"} + + client = JiraClient(auth_token=fake_auth_token) + + mock_httpx_client.get.return_value = httpx.Response( + status_code=200, + json=[cloud], + ) + + response = await client._get_cloud_data_from_available_resources() + assert response == cloud + + +@pytest.mark.asyncio +async def test_get_cloud_data_from_available_resources_multiple_clouds( + mock_httpx_client, fake_auth_token +): + cloud1 = {"id": "123", "name": "Test Cloud", "url": "https://test.atlassian.net"} + cloud2 = {"id": "456", "name": "Test Cloud 2", "url": "https://test2.atlassian.net"} + + client = JiraClient(auth_token=fake_auth_token) + + mock_httpx_client.get.return_value = httpx.Response( + status_code=200, + json=[cloud1, cloud2], + ) + + with pytest.raises(JiraToolExecutionError) as error: + await client._get_cloud_data_from_available_resources() + + assert "Multiple cloud IDs returned by Atlassian" in error.value.message + assert json.dumps(cloud1) in error.value.message + assert json.dumps(cloud2) in error.value.message + + +@pytest.mark.asyncio +async def test_get_cloud_data_from_available_resources_duplicate_cloud( + mock_httpx_client, fake_auth_token +): + cloud = {"id": "123", "name": "Test Cloud", "url": "https://test.atlassian.net"} + + client = JiraClient(auth_token=fake_auth_token) + + mock_httpx_client.get.return_value = httpx.Response( + status_code=200, + json=[cloud, cloud], + ) + + response = await client._get_cloud_data_from_available_resources() + assert response == cloud