"""Test cases for search_issues V3 API client and server integration"""
import asyncio
from unittest.mock import Mock, patch, AsyncMock
import pytest
from src.mcp_server_jira.jira_v3_api import JiraV3APIClient
from src.mcp_server_jira.server import JiraServer, JiraIssueResult
class TestSearchIssuesV3API:
"""Test suite for search_issues V3 API client"""
@pytest.mark.asyncio
async def test_v3_api_search_issues_success(self):
"""Test successful search issues request with V3 API"""
# Mock successful search response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"issues": [
{
"key": "PROJ-123",
"fields": {
"summary": "Test issue summary",
"description": "Test issue description",
"status": {"name": "Open"},
"assignee": {"displayName": "John Doe"},
"reporter": {"displayName": "Jane Smith"},
"created": "2023-01-01T00:00:00.000+0000",
"updated": "2023-01-02T00:00:00.000+0000"
}
},
{
"key": "PROJ-124",
"fields": {
"summary": "Another test issue",
"description": "Another description",
"status": {"name": "In Progress"},
"assignee": None,
"reporter": {"displayName": "Bob Wilson"},
"created": "2023-01-03T00:00:00.000+0000",
"updated": "2023-01-04T00:00:00.000+0000"
}
}
],
"startAt": 0,
"maxResults": 50,
"total": 2,
"isLast": True
}
mock_response.text = ""
mock_response.raise_for_status.return_value = None
# Mock httpx client
mock_client = AsyncMock()
mock_client.request.return_value = mock_response
client = JiraV3APIClient(
server_url="https://test.atlassian.net",
username="testuser",
token="testtoken"
)
# Replace the client instance
client.client = mock_client
result = await client.search_issues(
jql="project = PROJ",
max_results=10
)
# Verify the request was made correctly
mock_client.request.assert_called_once()
call_args = mock_client.request.call_args
assert call_args[1]["method"] == "GET"
assert call_args[1]["url"] == "https://test.atlassian.net/rest/api/3/search/jql"
assert call_args[1]["params"]["jql"] == "project = PROJ"
assert call_args[1]["params"]["maxResults"] == 10
# Verify response
assert result["total"] == 2
assert len(result["issues"]) == 2
assert result["issues"][0]["key"] == "PROJ-123"
@pytest.mark.asyncio
async def test_v3_api_search_issues_with_parameters(self):
"""Test search issues with optional parameters"""
# Mock successful search response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"issues": [],
"startAt": 0,
"maxResults": 25,
"total": 0,
"isLast": True
}
mock_response.text = ""
mock_response.raise_for_status.return_value = None
# Mock httpx client
mock_client = AsyncMock()
mock_client.request.return_value = mock_response
client = JiraV3APIClient(
server_url="https://test.atlassian.net",
username="testuser",
token="testtoken"
)
# Replace the client instance
client.client = mock_client
result = await client.search_issues(
jql="project = PROJ AND status = Open",
start_at=10,
max_results=25,
fields="summary,status,assignee",
expand="changelog"
)
# Verify the request was made correctly
mock_client.request.assert_called_once()
call_args = mock_client.request.call_args
assert call_args[1]["method"] == "GET"
assert call_args[1]["url"] == "https://test.atlassian.net/rest/api/3/search/jql"
params = call_args[1]["params"]
assert params["jql"] == "project = PROJ AND status = Open"
assert params["startAt"] == 10
assert params["maxResults"] == 25
assert params["fields"] == "summary,status,assignee"
assert params["expand"] == "changelog"
@pytest.mark.asyncio
async def test_v3_api_search_issues_missing_jql(self):
"""Test search issues with missing JQL parameter"""
client = JiraV3APIClient(
server_url="https://test.atlassian.net",
username="testuser",
token="testtoken"
)
with pytest.raises(ValueError, match="jql parameter is required"):
await client.search_issues("")
@pytest.mark.asyncio
async def test_v3_api_search_issues_api_error(self):
"""Test search issues with API error response"""
# Mock error response
mock_response = Mock()
mock_response.status_code = 400
mock_response.reason_phrase = "Bad Request"
mock_response.json.return_value = {"errorMessages": ["Invalid JQL"]}
from httpx import HTTPStatusError, Request, Response
mock_request = Mock(spec=Request)
mock_request.url = "https://test.atlassian.net/rest/api/3/search/jql"
# Mock httpx client
mock_client = AsyncMock()
mock_client.request.side_effect = HTTPStatusError(
"400 Bad Request", request=mock_request, response=mock_response
)
client = JiraV3APIClient(
server_url="https://test.atlassian.net",
username="testuser",
token="testtoken"
)
# Replace the client instance
client.client = mock_client
with pytest.raises(ValueError, match="Jira API returned an error: 400"):
await client.search_issues(jql="invalid jql syntax")
class TestSearchIssuesJiraServer:
"""Test suite for search_issues in JiraServer class"""
@pytest.mark.asyncio
async def test_server_search_issues_success(self):
"""Test JiraServer search_issues method with successful V3 API response"""
# Mock V3 API response
mock_v3_response = {
"issues": [
{
"key": "TEST-1",
"fields": {
"summary": "Test Summary",
"description": "Test Description",
"status": {"name": "Open"},
"assignee": {"displayName": "Test User"},
"reporter": {"displayName": "Reporter User"},
"created": "2023-01-01T00:00:00.000+0000",
"updated": "2023-01-02T00:00:00.000+0000"
}
}
]
}
# Mock V3 API client
mock_v3_client = AsyncMock()
mock_v3_client.search_issues.return_value = mock_v3_response
# Create JiraServer instance and mock the V3 client
server = JiraServer()
server.server_url = "https://test.atlassian.net"
server.username = "testuser"
server.token = "testtoken"
with patch.object(server, '_get_v3_api_client', return_value=mock_v3_client):
result = await server.search_jira_issues("project = TEST", max_results=10)
# Verify the result
assert isinstance(result, list)
assert len(result) == 1
issue = result[0]
assert isinstance(issue, dict)
assert issue["key"] == "TEST-1"
assert issue["fields"]["summary"] == "Test Summary"
assert issue["fields"]["description"] == "Test Description"
assert issue["fields"]["status"]["name"] == "Open"
assert issue["fields"]["assignee"]["displayName"] == "Test User"
assert issue["fields"]["reporter"]["displayName"] == "Reporter User"
assert issue["fields"]["created"] == "2023-01-01T00:00:00.000+0000"
assert issue["fields"]["updated"] == "2023-01-02T00:00:00.000+0000"
# Verify V3 client was called correctly
mock_v3_client.search_issues.assert_called_once_with(
jql="project = TEST", start_at=0, max_results=10
)
@pytest.mark.asyncio
async def test_server_search_issues_handles_missing_fields(self):
"""Test JiraServer search_issues method handles missing optional fields gracefully"""
# Mock V3 API response with minimal data
mock_v3_response = {
"issues": [
{
"key": "TEST-2",
"fields": {
"summary": "Basic Summary",
# Missing description, status, assignee, reporter, etc.
}
}
]
}
# Mock V3 API client
mock_v3_client = AsyncMock()
mock_v3_client.search_issues.return_value = mock_v3_response
# Create JiraServer instance and mock the V3 client
server = JiraServer()
server.server_url = "https://test.atlassian.net"
server.username = "testuser"
server.token = "testtoken"
with patch.object(server, '_get_v3_api_client', return_value=mock_v3_client):
result = await server.search_jira_issues("project = TEST")
# Verify the result handles missing fields gracefully
assert isinstance(result, list)
assert len(result) == 1
issue = result[0]
assert isinstance(issue, dict)
assert issue["key"] == "TEST-2"
assert issue["fields"]["summary"] == "Basic Summary"
# Missing description, status, assignee, reporter should be absent or None
assert issue["fields"].get("description") is None
assert issue["fields"].get("status") is None
assert issue["fields"].get("assignee") is None
assert issue["fields"].get("reporter") is None
@pytest.mark.asyncio
async def test_server_search_issues_api_error(self):
"""Test JiraServer search_issues method with API error"""
# Mock V3 API client that raises an error
mock_v3_client = AsyncMock()
mock_v3_client.search_issues.side_effect = ValueError("API connection failed")
# Create JiraServer instance and mock the V3 client
server = JiraServer()
server.server_url = "https://test.atlassian.net"
server.username = "testuser"
server.token = "testtoken"
with patch.object(server, '_get_v3_api_client', return_value=mock_v3_client):
with pytest.raises(ValueError, match="Failed to search issues"):
await server.search_jira_issues("project = TEST")
@pytest.mark.asyncio
async def test_server_search_issues_pagination(self):
"""Test JiraServer search_issues method handles pagination correctly"""
# Mock V3 API responses for pagination
# First page response
page1_response = {
"issues": [
{
"key": "TEST-1",
"fields": {
"summary": "First Issue",
"description": "First Description",
"status": {"name": "Open"},
"assignee": {"displayName": "User 1"},
"reporter": {"displayName": "Reporter 1"},
"created": "2023-01-01T00:00:00.000+0000",
"updated": "2023-01-01T00:00:00.000+0000"
}
},
{
"key": "TEST-2",
"fields": {
"summary": "Second Issue",
"description": "Second Description",
"status": {"name": "In Progress"},
"assignee": {"displayName": "User 2"},
"reporter": {"displayName": "Reporter 2"},
"created": "2023-01-02T00:00:00.000+0000",
"updated": "2023-01-02T00:00:00.000+0000"
}
}
],
"startAt": 0,
"maxResults": 2,
"total": 5,
"isLast": False
}
# Second page response
page2_response = {
"issues": [
{
"key": "TEST-3",
"fields": {
"summary": "Third Issue",
"description": "Third Description",
"status": {"name": "Done"},
"assignee": {"displayName": "User 3"},
"reporter": {"displayName": "Reporter 3"},
"created": "2023-01-03T00:00:00.000+0000",
"updated": "2023-01-03T00:00:00.000+0000"
}
},
{
"key": "TEST-4",
"fields": {
"summary": "Fourth Issue",
"description": "Fourth Description",
"status": {"name": "Closed"},
"assignee": None,
"reporter": {"displayName": "Reporter 4"},
"created": "2023-01-04T00:00:00.000+0000",
"updated": "2023-01-04T00:00:00.000+0000"
}
}
],
"startAt": 2,
"maxResults": 2,
"total": 5,
"isLast": False
}
# Third page response
page3_response = {
"issues": [
{
"key": "TEST-5",
"fields": {
"summary": "Fifth Issue",
"description": "Fifth Description",
"status": {"name": "Open"},
"assignee": {"displayName": "User 5"},
"reporter": {"displayName": "Reporter 5"},
"created": "2023-01-05T00:00:00.000+0000",
"updated": "2023-01-05T00:00:00.000+0000"
}
}
],
"startAt": 4,
"maxResults": 2,
"total": 5,
"isLast": True
}
# Mock V3 API client with side_effect to return different pages
mock_v3_client = AsyncMock()
mock_v3_client.search_issues.side_effect = [page1_response, page2_response, page3_response]
# Create JiraServer instance and mock the V3 client
server = JiraServer()
server.server_url = "https://test.atlassian.net"
server.username = "testuser"
server.token = "testtoken"
with patch.object(server, '_get_v3_api_client', return_value=mock_v3_client):
result = await server.search_jira_issues("project = TEST", max_results=10)
# Verify all issues from all pages were retrieved
assert isinstance(result, list)
assert len(result) == 5
# Check each issue dict
assert isinstance(result[0], dict)
assert result[0]["key"] == "TEST-1"
assert result[0]["fields"]["summary"] == "First Issue"
assert result[0]["fields"]["status"]["name"] == "Open"
assert result[1]["key"] == "TEST-2"
assert result[1]["fields"]["summary"] == "Second Issue"
assert result[1]["fields"]["status"]["name"] == "In Progress"
assert result[2]["key"] == "TEST-3"
assert result[2]["fields"]["summary"] == "Third Issue"
assert result[2]["fields"]["status"]["name"] == "Done"
assert result[3]["key"] == "TEST-4"
assert result[3]["fields"]["summary"] == "Fourth Issue"
assert result[3]["fields"]["status"]["name"] == "Closed"
# None handling
assert result[3]["fields"].get("assignee") is None
assert result[4]["key"] == "TEST-5"
assert result[4]["fields"]["summary"] == "Fifth Issue"
assert result[4]["fields"]["status"]["name"] == "Open"
# Verify V3 client was called the correct number of times with correct parameters
assert mock_v3_client.search_issues.call_count == 3
# Check first call
first_call = mock_v3_client.search_issues.call_args_list[0]
assert first_call[1]["jql"] == "project = TEST"
assert first_call[1]["start_at"] == 0
assert first_call[1]["max_results"] == 10
# Check second call
second_call = mock_v3_client.search_issues.call_args_list[1]
assert second_call[1]["jql"] == "project = TEST"
assert second_call[1]["start_at"] == 2 # After first 2 issues
assert second_call[1]["max_results"] == 8 # Remaining needed: 10 - 2 = 8, min(8, 100) = 8
# Check third call
third_call = mock_v3_client.search_issues.call_args_list[2]
assert third_call[1]["jql"] == "project = TEST"
assert third_call[1]["start_at"] == 4 # After first 4 issues
assert third_call[1]["max_results"] == 6 # Remaining needed: 10 - 4 = 6, min(6, 100) = 6
@pytest.mark.asyncio
async def test_server_search_issues_pagination_with_limit(self):
"""Test JiraServer search_issues method respects max_results when paginating"""
# Mock V3 API responses for multiple pages, but we'll limit results
page1_response = {
"issues": [
{"key": "TEST-1", "fields": {"summary": "First Issue"}},
{"key": "TEST-2", "fields": {"summary": "Second Issue"}},
{"key": "TEST-3", "fields": {"summary": "Third Issue"}}
],
"startAt": 0,
"maxResults": 3,
"total": 10,
"isLast": False
}
page2_response = {
"issues": [
{"key": "TEST-4", "fields": {"summary": "Fourth Issue"}},
{"key": "TEST-5", "fields": {"summary": "Fifth Issue"}}
],
"startAt": 3,
"maxResults": 2, # Only 2 more to reach our limit of 5
"total": 10,
"isLast": False
}
# Mock V3 API client
mock_v3_client = AsyncMock()
mock_v3_client.search_issues.side_effect = [page1_response, page2_response]
# Create JiraServer instance and mock the V3 client
server = JiraServer()
server.server_url = "https://test.atlassian.net"
server.username = "testuser"
server.token = "testtoken"
with patch.object(server, '_get_v3_api_client', return_value=mock_v3_client):
# Request only 5 results max
result = await server.search_jira_issues("project = TEST", max_results=5)
# Verify exactly 5 issues were returned (respecting max_results)
assert isinstance(result, list)
assert len(result) == 5
assert result[0]["key"] == "TEST-1"
assert result[1]["key"] == "TEST-2"
assert result[2]["key"] == "TEST-3"
assert result[3]["key"] == "TEST-4"
assert result[4]["key"] == "TEST-5"
# Verify pagination stopped at the right point
assert mock_v3_client.search_issues.call_count == 2