This PR adds a simplified Linear toolkit focused on core functionality: ## What's Included **Tools:** - `get_issue` - Get detailed information about Linear issues - `get_teams` - Get team information and details **Models:** - `DateRange` enum with comprehensive date range support (TODAY, YESTERDAY, THIS_WEEK, LAST_WEEK, THIS_MONTH, LAST_MONTH, THIS_YEAR, LAST_YEAR, LAST_7_DAYS, LAST_30_DAYS) - Timezone-aware datetime handling following Google toolkit patterns ## What's Simplified This toolkit has been streamlined by removing: - Cycles management tools - Projects management tools - Users management tools - Workflows management tools - Corresponding tests and evaluations for removed features ## Quality Assurance - All linting and formatting checks pass - Comprehensive test coverage for included functionality - Follows established patterns from Google toolkit --------- Co-authored-by: Eric Gustin <34000337+EricGustin@users.noreply.github.com>
154 lines
5.4 KiB
Python
154 lines
5.4 KiB
Python
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from arcade_tdk.errors import ToolExecutionError
|
|
|
|
from arcade_linear.client import LinearClient
|
|
|
|
|
|
class TestLinearClient:
|
|
"""Tests for LinearClient"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_client_init(self):
|
|
"""Test client initialization"""
|
|
test_token = "test_token" # noqa: S105
|
|
client = LinearClient(test_token)
|
|
assert client.auth_token == test_token
|
|
assert client.api_url == "https://api.linear.app/graphql"
|
|
assert client.max_concurrent_requests == 3
|
|
assert client.timeout_seconds == 30
|
|
|
|
def test_build_headers(self):
|
|
"""Test header building"""
|
|
client = LinearClient("test_token")
|
|
headers = client._build_headers()
|
|
|
|
assert headers["Authorization"] == "Bearer test_token"
|
|
assert headers["Content-Type"] == "application/json"
|
|
assert headers["Accept"] == "application/json"
|
|
|
|
def test_build_error_message_graphql_single(self):
|
|
"""Test error message building for single GraphQL error"""
|
|
client = LinearClient("test_token")
|
|
|
|
# Mock response with single GraphQL error
|
|
response = AsyncMock()
|
|
response.status_code = 200
|
|
response.json = lambda: {
|
|
"errors": [{"message": "Field not found", "extensions": {"code": "FIELD_ERROR"}}]
|
|
}
|
|
|
|
user_msg, dev_msg = client._build_error_message(response)
|
|
|
|
assert user_msg == "Field not found"
|
|
assert "Field not found" in dev_msg
|
|
assert "FIELD_ERROR" in dev_msg
|
|
|
|
def test_build_error_message_http_error(self):
|
|
"""Test error message building for HTTP errors"""
|
|
client = LinearClient("test_token")
|
|
|
|
# Mock HTTP error response with valid JSON but no GraphQL errors
|
|
response = AsyncMock()
|
|
response.status_code = 401
|
|
response.reason_phrase = "Unauthorized"
|
|
response.text = "Authentication required"
|
|
|
|
# Return valid JSON that doesn't have "errors" field
|
|
response.json = lambda: {"message": "Authentication required"}
|
|
|
|
user_msg, dev_msg = client._build_error_message(response)
|
|
|
|
assert user_msg == "HTTP 401: Unauthorized"
|
|
assert "HTTP 401: Authentication required" in dev_msg
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_raise_for_status_success(self):
|
|
"""Test _raise_for_status with successful response"""
|
|
client = LinearClient("test_token")
|
|
|
|
# Mock successful response
|
|
response = AsyncMock()
|
|
response.status_code = 200
|
|
response.json = lambda: {"data": {"viewer": {"id": "user_1"}}}
|
|
|
|
# Should not raise exception
|
|
client._raise_for_status(response)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_raise_for_status_graphql_error(self):
|
|
"""Test _raise_for_status with GraphQL errors"""
|
|
client = LinearClient("test_token")
|
|
|
|
# Mock response with GraphQL errors
|
|
response = AsyncMock()
|
|
response.status_code = 200
|
|
response.json = lambda: {"errors": [{"message": "Invalid field"}]}
|
|
|
|
with pytest.raises(ToolExecutionError) as exc_info:
|
|
client._raise_for_status(response)
|
|
|
|
assert "Invalid field" in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("httpx.AsyncClient")
|
|
async def test_execute_query_success(self, mock_http_client):
|
|
"""Test successful GraphQL query execution"""
|
|
client = LinearClient("test_token")
|
|
|
|
# Mock HTTP client and response
|
|
mock_client_instance = AsyncMock()
|
|
mock_http_client.return_value.__aenter__.return_value = mock_client_instance
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json = lambda: {"data": {"viewer": {"id": "user_1"}}}
|
|
mock_client_instance.post.return_value = mock_response
|
|
|
|
query = "query { viewer { id } }"
|
|
variables = {"test": "value"}
|
|
|
|
result = await client.execute_query(query, variables)
|
|
|
|
assert result["data"]["viewer"]["id"] == "user_1"
|
|
mock_client_instance.post.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_linear.client.LinearClient.execute_query")
|
|
async def test_get_teams(self, mock_execute_query):
|
|
"""Test get_teams method"""
|
|
client = LinearClient("test_token")
|
|
|
|
mock_execute_query.return_value = {
|
|
"data": {
|
|
"teams": {
|
|
"nodes": [{"id": "team_1", "name": "Frontend", "key": "FE"}],
|
|
"pageInfo": {"hasNextPage": False},
|
|
}
|
|
}
|
|
}
|
|
|
|
result = await client.get_teams(first=10, include_archived=True)
|
|
|
|
assert len(result["nodes"]) == 1
|
|
assert result["nodes"][0]["name"] == "Frontend"
|
|
assert result["pageInfo"]["hasNextPage"] is False
|
|
mock_execute_query.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_linear.client.LinearClient.execute_query")
|
|
async def test_get_issue_by_id(self, mock_execute_query):
|
|
"""Test get_issue_by_id method"""
|
|
client = LinearClient("test_token")
|
|
|
|
mock_execute_query.return_value = {
|
|
"data": {"issue": {"id": "issue_1", "identifier": "FE-123", "title": "Test issue"}}
|
|
}
|
|
|
|
result = await client.get_issue_by_id("FE-123")
|
|
|
|
assert result["id"] == "issue_1"
|
|
assert result["identifier"] == "FE-123"
|
|
assert result["title"] == "Test issue"
|
|
mock_execute_query.assert_called_once()
|