We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/sooperset/mcp-atlassian'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Tests for the Jira ProjectsMixin."""
from typing import Any
from unittest.mock import MagicMock, call, patch
import pytest
from mcp_atlassian.jira import JiraFetcher
from mcp_atlassian.jira.config import JiraConfig
from mcp_atlassian.jira.projects import ProjectsMixin
from mcp_atlassian.models.jira.issue import JiraIssue
from mcp_atlassian.models.jira.search import JiraSearchResult
@pytest.fixture
def mock_config():
"""Fixture to create a mock JiraConfig instance."""
config = MagicMock(spec=JiraConfig)
config.url = "https://test.atlassian.net"
config.username = "test@example.com"
config.api_token = "test-token"
config.auth_type = "pat"
return config
@pytest.fixture
def projects_mixin(jira_fetcher: JiraFetcher) -> ProjectsMixin:
"""Fixture to create a ProjectsMixin instance for testing."""
mixin = jira_fetcher
return mixin
@pytest.fixture
def mock_projects():
"""Fixture to return mock project data."""
return [
{
"id": "10000",
"key": "PROJ1",
"name": "Project One",
"lead": {"name": "user1", "displayName": "User One"},
},
{
"id": "10001",
"key": "PROJ2",
"name": "Project Two",
"lead": {"name": "user2", "displayName": "User Two"},
},
]
@pytest.fixture
def mock_components():
"""Fixture to return mock project components."""
return [
{"id": "10000", "name": "Component One"},
{"id": "10001", "name": "Component Two"},
]
@pytest.fixture
def mock_versions():
"""Fixture to return mock project versions."""
return [
{"id": "10000", "name": "1.0", "released": True},
{"id": "10001", "name": "2.0", "released": False},
]
@pytest.fixture
def mock_roles():
"""Fixture to return mock project roles."""
return {
"Administrator": {"id": "10000", "name": "Administrator"},
"Developer": {"id": "10001", "name": "Developer"},
}
@pytest.fixture
def mock_role_members():
"""Fixture to return mock project role members."""
return {
"actors": [
{"id": "user1", "name": "user1", "displayName": "User One"},
{"id": "user2", "name": "user2", "displayName": "User Two"},
]
}
@pytest.fixture
def mock_issue_types():
"""Fixture to return mock issue types."""
return [
{"id": "10000", "name": "Bug", "description": "A bug"},
{"id": "10001", "name": "Task", "description": "A task"},
]
def test_get_all_projects(projects_mixin: ProjectsMixin, mock_projects: list[dict]):
"""Test get_all_projects method."""
projects_mixin.jira.projects.return_value = mock_projects
# Test with default value (include_archived=False)
result = projects_mixin.get_all_projects()
assert result == mock_projects
projects_mixin.jira.projects.assert_called_once_with(included_archived=False)
# Reset mock and test with include_archived=True
projects_mixin.jira.projects.reset_mock()
projects_mixin.jira.projects.return_value = mock_projects
result = projects_mixin.get_all_projects(include_archived=True)
assert result == mock_projects
projects_mixin.jira.projects.assert_called_once_with(included_archived=True)
def test_get_all_projects_exception(projects_mixin: ProjectsMixin):
"""Test get_all_projects method with exception."""
projects_mixin.jira.projects.side_effect = Exception("API error")
result = projects_mixin.get_all_projects()
assert result == []
projects_mixin.jira.projects.assert_called_once()
def test_get_all_projects_non_list_response(projects_mixin: ProjectsMixin):
"""Test get_all_projects method with non-list response."""
projects_mixin.jira.projects.return_value = "not a list"
result = projects_mixin.get_all_projects()
assert result == []
projects_mixin.jira.projects.assert_called_once()
def test_get_project(projects_mixin: ProjectsMixin, mock_projects: list[dict]):
"""Test get_project method."""
project = mock_projects[0]
projects_mixin.jira.project.return_value = project
result = projects_mixin.get_project("PROJ1")
assert result == project
projects_mixin.jira.project.assert_called_once_with("PROJ1")
def test_get_project_exception(projects_mixin: ProjectsMixin):
"""Test get_project method with exception."""
projects_mixin.jira.project.side_effect = Exception("API error")
result = projects_mixin.get_project("PROJ1")
assert result is None
projects_mixin.jira.project.assert_called_once()
def test_get_project_issues(projects_mixin: ProjectsMixin):
"""Test get_project_issues method."""
# Setup mock response
# Mock the method that is actually called: search_issues
# It should return a JiraSearchResult object
mock_search_result = JiraSearchResult(
issues=[], total=0, start_at=0, max_results=50
)
projects_mixin.search_issues = MagicMock(return_value=mock_search_result)
# Call the method
result = projects_mixin.get_project_issues("TEST")
# Verify search_issues was called, not jira.jql
projects_mixin.search_issues.assert_called_once_with(
'project = "TEST"',
start=0,
limit=50,
)
assert isinstance(result, JiraSearchResult)
assert len(result.issues) == 0
def test_get_project_issues_with_start(projects_mixin: ProjectsMixin) -> None:
"""Test getting project issues with a start index."""
# Mock search_issues to return a result reflecting pagination
mock_issue = JiraIssue(key="PROJ-2", summary="Issue 2", id="10002")
mock_search_result = JiraSearchResult(
issues=[mock_issue],
total=1,
start_at=3,
max_results=5,
)
projects_mixin.search_issues = MagicMock(return_value=mock_search_result)
project_key = "PROJ"
start_index = 3
# Call the method
result = projects_mixin.get_project_issues(project_key, start=start_index, limit=5)
assert len(result.issues) == 1
# Note: Assertions on start_at, max_results, total should be based on the
# JiraSearchResult object returned by the mocked search_issues
assert result.start_at == 3 # Comes from the mocked JiraSearchResult
assert result.max_results == 5 # Comes from the mocked JiraSearchResult
assert result.total == 1 # Comes from the mocked JiraSearchResult
# Verify search_issues was called with the correct arguments
projects_mixin.search_issues.assert_called_once_with(
f'project = "{project_key}"',
start=start_index,
limit=5,
)
def test_project_exists(projects_mixin: ProjectsMixin, mock_projects: list[dict]):
"""Test project_exists method."""
# Test with existing project
project = mock_projects[0]
projects_mixin.jira.project.return_value = project
result = projects_mixin.project_exists("PROJ1")
assert result is True
projects_mixin.jira.project.assert_called_once_with("PROJ1")
# Test with non-existing project
projects_mixin.jira.project.reset_mock()
projects_mixin.jira.project.return_value = None
result = projects_mixin.project_exists("NONEXISTENT")
assert result is False
projects_mixin.jira.project.assert_called_once()
def test_project_exists_exception(projects_mixin: ProjectsMixin):
"""Test project_exists method with exception."""
projects_mixin.jira.project.side_effect = Exception("API error")
result = projects_mixin.project_exists("PROJ1")
assert result is False
projects_mixin.jira.project.assert_called_once()
def test_get_project_components(
projects_mixin: ProjectsMixin, mock_components: list[dict]
):
"""Test get_project_components method."""
projects_mixin.jira.get_project_components.return_value = mock_components
result = projects_mixin.get_project_components("PROJ1")
assert result == mock_components
projects_mixin.jira.get_project_components.assert_called_once_with(key="PROJ1")
def test_get_project_components_exception(projects_mixin: ProjectsMixin):
"""Test get_project_components method with exception."""
projects_mixin.jira.get_project_components.side_effect = Exception("API error")
result = projects_mixin.get_project_components("PROJ1")
assert result == []
projects_mixin.jira.get_project_components.assert_called_once()
def test_get_project_components_non_list_response(projects_mixin: ProjectsMixin):
"""Test get_project_components method with non-list response."""
projects_mixin.jira.get_project_components.return_value = "not a list"
result = projects_mixin.get_project_components("PROJ1")
assert result == []
projects_mixin.jira.get_project_components.assert_called_once()
def test_get_project_versions(projects_mixin: ProjectsMixin, mock_versions: list[dict]):
"""Test get_project_versions method."""
projects_mixin.jira.get_project_versions.return_value = mock_versions
# Simplified dicts should include id, name, released and archived
expected = [
{
"id": v["id"],
"name": v["name"],
"released": v.get("released", False),
"archived": v.get("archived", False),
}
for v in mock_versions
]
result = projects_mixin.get_project_versions("PROJ1")
assert result == expected
projects_mixin.jira.get_project_versions.assert_called_once_with(key="PROJ1")
def test_get_project_versions_exception(projects_mixin: ProjectsMixin):
"""Test get_project_versions method with exception."""
projects_mixin.jira.get_project_versions.side_effect = Exception("API error")
result = projects_mixin.get_project_versions("PROJ1")
assert result == []
projects_mixin.jira.get_project_versions.assert_called_once_with(key="PROJ1")
def test_get_project_versions_non_list_response(projects_mixin: ProjectsMixin):
"""Test get_project_versions method with non-list response."""
projects_mixin.jira.get_project_versions.return_value = "not a list"
result = projects_mixin.get_project_versions("PROJ1")
assert result == []
projects_mixin.jira.get_project_versions.assert_called_once_with(key="PROJ1")
def test_get_project_roles(
projects_mixin: ProjectsMixin, mock_roles: dict[str, dict[str, str]]
):
"""Test get_project_roles method."""
projects_mixin.jira.get_project_roles.return_value = mock_roles
result = projects_mixin.get_project_roles("PROJ1")
assert result == mock_roles
projects_mixin.jira.get_project_roles.assert_called_once_with(project_key="PROJ1")
def test_get_project_roles_exception(projects_mixin: ProjectsMixin):
"""Test get_project_roles method with exception."""
projects_mixin.jira.get_project_roles.side_effect = Exception("API error")
result = projects_mixin.get_project_roles("PROJ1")
assert result == {}
projects_mixin.jira.get_project_roles.assert_called_once()
def test_get_project_roles_non_dict_response(projects_mixin: ProjectsMixin):
"""Test get_project_roles method with non-dict response."""
projects_mixin.jira.get_project_roles.return_value = "not a dict"
result = projects_mixin.get_project_roles("PROJ1")
assert result == {}
projects_mixin.jira.get_project_roles.assert_called_once()
def test_get_project_role_members(
projects_mixin: ProjectsMixin, mock_role_members: dict[str, list[dict[str, str]]]
):
"""Test get_project_role_members method."""
projects_mixin.jira.get_project_actors_for_role_project.return_value = (
mock_role_members
)
result = projects_mixin.get_project_role_members("PROJ1", "10001")
assert result == mock_role_members["actors"]
projects_mixin.jira.get_project_actors_for_role_project.assert_called_once_with(
project_key="PROJ1", role_id="10001"
)
def test_get_project_role_members_exception(projects_mixin: ProjectsMixin):
"""Test get_project_role_members method with exception."""
projects_mixin.jira.get_project_actors_for_role_project.side_effect = Exception(
"API error"
)
result = projects_mixin.get_project_role_members("PROJ1", "10001")
assert result == []
projects_mixin.jira.get_project_actors_for_role_project.assert_called_once()
def test_get_project_role_members_invalid_response(projects_mixin: ProjectsMixin):
"""Test get_project_role_members method with invalid response."""
# Response without actors
projects_mixin.jira.get_project_actors_for_role_project.return_value = {}
result = projects_mixin.get_project_role_members("PROJ1", "10001")
assert result == []
projects_mixin.jira.get_project_actors_for_role_project.assert_called_once()
# Non-dict response
projects_mixin.jira.get_project_actors_for_role_project.reset_mock()
projects_mixin.jira.get_project_actors_for_role_project.return_value = "not a dict"
result = projects_mixin.get_project_role_members("PROJ1", "10001")
assert result == []
def test_get_project_permission_scheme(projects_mixin: ProjectsMixin):
"""Test get_project_permission_scheme method."""
scheme = {"id": "10000", "name": "Default Permission Scheme"}
projects_mixin.jira.get_project_permission_scheme.return_value = scheme
result = projects_mixin.get_project_permission_scheme("PROJ1")
assert result == scheme
projects_mixin.jira.get_project_permission_scheme.assert_called_once_with(
project_id_or_key="PROJ1"
)
def test_get_project_permission_scheme_exception(projects_mixin: ProjectsMixin):
"""Test get_project_permission_scheme method with exception."""
projects_mixin.jira.get_project_permission_scheme.side_effect = Exception(
"API error"
)
result = projects_mixin.get_project_permission_scheme("PROJ1")
assert result is None
projects_mixin.jira.get_project_permission_scheme.assert_called_once()
def test_get_project_notification_scheme(projects_mixin: ProjectsMixin):
"""Test get_project_notification_scheme method."""
scheme = {"id": "10000", "name": "Default Notification Scheme"}
projects_mixin.jira.get_project_notification_scheme.return_value = scheme
result = projects_mixin.get_project_notification_scheme("PROJ1")
assert result == scheme
projects_mixin.jira.get_project_notification_scheme.assert_called_once_with(
project_id_or_key="PROJ1"
)
def test_get_project_notification_scheme_exception(projects_mixin: ProjectsMixin):
"""Test get_project_notification_scheme method with exception."""
projects_mixin.jira.get_project_notification_scheme.side_effect = Exception(
"API error"
)
result = projects_mixin.get_project_notification_scheme("PROJ1")
assert result is None
projects_mixin.jira.get_project_notification_scheme.assert_called_once()
def test_get_project_issue_types(
projects_mixin: ProjectsMixin, mock_issue_types: list[dict]
):
"""Test get_project_issue_types with new paginated values format."""
createmeta = {
"maxResults": 50,
"startAt": 0,
"total": len(mock_issue_types),
"isLast": True,
"values": mock_issue_types,
}
projects_mixin.jira.issue_createmeta_issuetypes.return_value = createmeta
result = projects_mixin.get_project_issue_types("PROJ1")
assert result == mock_issue_types
projects_mixin.jira.issue_createmeta_issuetypes.assert_called_once_with(
project="PROJ1"
)
def test_get_project_issue_types_old_format(
projects_mixin: ProjectsMixin, mock_issue_types: list[dict]
):
"""Test get_project_issue_types falls back to old projects format."""
createmeta = {
"projects": [
{"key": "PROJ1", "name": "Project One", "issuetypes": mock_issue_types}
]
}
projects_mixin.jira.issue_createmeta_issuetypes.return_value = createmeta
result = projects_mixin.get_project_issue_types("PROJ1")
assert result == mock_issue_types
def test_get_project_issue_types_empty_response(projects_mixin: ProjectsMixin):
"""Test get_project_issue_types method with empty response."""
# Empty values list
projects_mixin.jira.issue_createmeta_issuetypes.return_value = {
"values": [],
"total": 0,
}
result = projects_mixin.get_project_issue_types("PROJ1")
assert result == []
projects_mixin.jira.issue_createmeta_issuetypes.assert_called_once()
# Empty old format fallback
projects_mixin.jira.issue_createmeta_issuetypes.reset_mock()
projects_mixin.jira.issue_createmeta_issuetypes.return_value = {"projects": []}
result = projects_mixin.get_project_issue_types("PROJ1")
assert result == []
def test_get_project_issue_types_exception(projects_mixin: ProjectsMixin):
"""Test get_project_issue_types method with exception."""
projects_mixin.jira.issue_createmeta_issuetypes.side_effect = Exception("API error")
result = projects_mixin.get_project_issue_types("PROJ1")
assert result == []
projects_mixin.jira.issue_createmeta_issuetypes.assert_called_once()
def test_get_project_issues_count(projects_mixin: ProjectsMixin):
"""Test get_project_issues_count method."""
jql_result = {"total": 42}
projects_mixin.jira.jql.return_value = jql_result
result = projects_mixin.get_project_issues_count("PROJ1")
assert result == 42
projects_mixin.jira.jql.assert_called_once_with(
jql='project = "PROJ1"', fields="key", limit=1
)
def test_get_project_issues_count__project_with_reserved_keyword(
projects_mixin: ProjectsMixin,
):
"""Test get_project_issues_count method."""
jql_result = {"total": 42}
projects_mixin.jira.jql.return_value = jql_result
result = projects_mixin.get_project_issues_count("AND")
assert result == 42
projects_mixin.jira.jql.assert_called_once_with(
jql='project = "AND"', fields="key", limit=1
)
def test_get_project_issues_count_invalid_response(projects_mixin: ProjectsMixin):
"""Test get_project_issues_count method with invalid response."""
# No total field
projects_mixin.jira.jql.return_value = {}
result = projects_mixin.get_project_issues_count("PROJ1")
assert result == 0
projects_mixin.jira.jql.assert_called_once()
# Non-dict response
projects_mixin.jira.jql.reset_mock()
projects_mixin.jira.jql.return_value = "not a dict"
result = projects_mixin.get_project_issues_count("PROJ1")
assert result == 0
projects_mixin.jira.jql.assert_called_once()
def test_get_project_issues_count_exception(projects_mixin: ProjectsMixin):
"""Test get_project_issues_count method with exception."""
projects_mixin.jira.jql.side_effect = Exception("API error")
result = projects_mixin.get_project_issues_count("PROJ1")
assert result == 0
projects_mixin.jira.jql.assert_called_once()
def test_get_project_issues_with_search_mixin(projects_mixin: ProjectsMixin):
"""Test get_project_issues method with search_issues available."""
# Mock the search_issues method
mock_search_result = [MagicMock(), MagicMock()]
projects_mixin.search_issues = MagicMock(return_value=mock_search_result)
result = projects_mixin.get_project_issues("PROJ1", start=10, limit=20)
assert result == mock_search_result
projects_mixin.search_issues.assert_called_once_with(
'project = "PROJ1"', start=10, limit=20
)
projects_mixin.jira.jql.assert_not_called()
def test_get_project_issues_invalid_response(projects_mixin: ProjectsMixin):
"""Test get_project_issues method with invalid response."""
# Mock search_issues to simulate an empty result scenario
mock_search_result = JiraSearchResult(
issues=[], total=0, start_at=0, max_results=50
)
projects_mixin.search_issues = MagicMock(return_value=mock_search_result)
result = projects_mixin.get_project_issues("PROJ1")
assert result.issues == []
projects_mixin.search_issues.assert_called_once()
# Reset mock and test with non-JiraSearchResult response (this would be handled by the except block)
projects_mixin.search_issues.reset_mock()
projects_mixin.search_issues = MagicMock(
side_effect=TypeError("Not a JiraSearchResult")
)
result = projects_mixin.get_project_issues("PROJ1")
assert result.issues == []
projects_mixin.search_issues.assert_called_once()
def test_get_project_issues_exception(projects_mixin: ProjectsMixin):
"""Test get_project_issues method with exception."""
# Mock search_issues to raise an exception, simulating an API error during the search
projects_mixin.search_issues = MagicMock(side_effect=Exception("API error"))
result = projects_mixin.get_project_issues("PROJ1")
assert result.issues == []
# Verify that search_issues was called, even though it raised an exception
# The except block in get_project_issues catches it and returns an empty result
projects_mixin.search_issues.assert_called_once()
def test_get_project_keys(
projects_mixin: ProjectsMixin, mock_projects: list[dict[str, Any]]
):
"""Test get_project_keys method."""
# Mock the get_all_projects method
with patch.object(projects_mixin, "get_all_projects", return_value=mock_projects):
result = projects_mixin.get_project_keys()
assert result == ["PROJ1", "PROJ2"]
projects_mixin.get_all_projects.assert_called_once()
def test_get_project_keys_exception(projects_mixin: ProjectsMixin):
"""Test get_project_keys method with exception."""
# Mock the get_all_projects method to raise an exception
with patch.object(
projects_mixin, "get_all_projects", side_effect=Exception("Error")
):
result = projects_mixin.get_project_keys()
assert result == []
projects_mixin.get_all_projects.assert_called_once()
def test_get_project_leads(
projects_mixin: ProjectsMixin, mock_projects: list[dict[str, Any]]
):
"""Test get_project_leads method."""
# Mock the get_all_projects method
with patch.object(projects_mixin, "get_all_projects", return_value=mock_projects):
result = projects_mixin.get_project_leads()
assert result == {"PROJ1": "user1", "PROJ2": "user2"}
projects_mixin.get_all_projects.assert_called_once()
def test_get_project_leads_with_different_lead_formats(
projects_mixin: ProjectsMixin, mock_projects: list[dict[str, Any]]
):
"""Test get_project_leads method with different lead formats."""
mixed_projects = [
# Project with lead as dictionary with name
{"key": "PROJ1", "lead": {"name": "user1", "displayName": "User One"}},
# Project with lead as dictionary with displayName but no name
{"key": "PROJ2", "lead": {"displayName": "User Two"}},
# Project with lead as string
{"key": "PROJ3", "lead": "user3"},
# Project without lead
{"key": "PROJ4"},
]
# Mock the get_all_projects method
with patch.object(projects_mixin, "get_all_projects", return_value=mixed_projects):
result = projects_mixin.get_project_leads()
assert result == {"PROJ1": "user1", "PROJ2": "User Two", "PROJ3": "user3"}
projects_mixin.get_all_projects.assert_called_once()
def test_get_project_leads_exception(projects_mixin: ProjectsMixin):
"""Test get_project_leads method with exception."""
# Mock the get_all_projects method to raise an exception
with patch.object(
projects_mixin, "get_all_projects", side_effect=Exception("Error")
):
result = projects_mixin.get_project_leads()
assert result == {}
projects_mixin.get_all_projects.assert_called_once()
def test_get_user_accessible_projects(
projects_mixin: ProjectsMixin, mock_projects: list[dict[str, Any]]
):
"""Test get_user_accessible_projects method."""
# Mock the get_all_projects method
with patch.object(projects_mixin, "get_all_projects", return_value=mock_projects):
# Set up the browse permission responses
browse_users_responses = [
[{"name": "test_user"}], # User has access to PROJ1
[], # User doesn't have access to PROJ2
]
projects_mixin.jira.get_users_with_browse_permission_to_a_project.side_effect = browse_users_responses
result = projects_mixin.get_user_accessible_projects("test_user")
# Only PROJ1 should be in the result
assert len(result) == 1
assert result[0]["key"] == "PROJ1"
# Check that get_users_with_browse_permission_to_a_project was called for both projects
assert (
projects_mixin.jira.get_users_with_browse_permission_to_a_project.call_count
== 2
)
projects_mixin.jira.get_users_with_browse_permission_to_a_project.assert_has_calls(
[
call(username="test_user", project_key="PROJ1", limit=1),
call(username="test_user", project_key="PROJ2", limit=1),
]
)
def test_get_user_accessible_projects_with_permissions_exception(
projects_mixin: ProjectsMixin, mock_projects: list[dict[str, Any]]
):
"""Test get_user_accessible_projects method with exception in permissions check."""
# Mock the get_all_projects method
with patch.object(projects_mixin, "get_all_projects", return_value=mock_projects):
# First call succeeds, second call raises exception
projects_mixin.jira.get_users_with_browse_permission_to_a_project.side_effect = [
[{"name": "test_user"}], # User has access to PROJ1
Exception("Permission error"), # Error checking PROJ2
]
result = projects_mixin.get_user_accessible_projects("test_user")
# Only PROJ1 should be in the result (PROJ2 was skipped due to error)
assert len(result) == 1
assert result[0]["key"] == "PROJ1"
def test_get_user_accessible_projects_exception(projects_mixin: ProjectsMixin):
"""Test get_user_accessible_projects method with main exception."""
# Mock the get_all_projects method to raise an exception
with patch.object(
projects_mixin, "get_all_projects", side_effect=Exception("Error")
):
result = projects_mixin.get_user_accessible_projects("test_user")
assert result == []
projects_mixin.get_all_projects.assert_called_once()
projects_mixin.jira.get_users_with_browse_permission_to_a_project.assert_not_called()
def test_create_project_version_minimal(projects_mixin: ProjectsMixin) -> None:
"""Test create_project_version with only required fields."""
mock_response = {"id": "201", "name": "v4.0"}
with patch.object(
projects_mixin, "create_version", return_value=mock_response
) as mock_create_version:
result = projects_mixin.create_project_version(project_key="PROJ2", name="v4.0")
assert result == mock_response
mock_create_version.assert_called_once_with(
project="PROJ2",
name="v4.0",
start_date=None,
release_date=None,
description=None,
)
def test_create_project_version_all_fields(projects_mixin: ProjectsMixin) -> None:
"""Test create_project_version with all fields."""
mock_response = {
"id": "202",
"name": "v5.0",
"description": "Release 5.0",
"startDate": "2025-08-01",
"releaseDate": "2025-08-15",
}
with patch.object(
projects_mixin, "create_version", return_value=mock_response
) as mock_create_version:
result = projects_mixin.create_project_version(
project_key="PROJ3",
name="v5.0",
start_date="2025-08-01",
release_date="2025-08-15",
description="Release 5.0",
)
assert result == mock_response
mock_create_version.assert_called_once_with(
project="PROJ3",
name="v5.0",
start_date="2025-08-01",
release_date="2025-08-15",
description="Release 5.0",
)
def test_create_project_version_error(projects_mixin: ProjectsMixin) -> None:
"""Test create_project_version propagates errors."""
with patch.object(
projects_mixin, "create_version", side_effect=Exception("API failure")
):
with pytest.raises(Exception):
projects_mixin.create_project_version("PROJ4", "v6.0")