#
tokens: 43365/50000 2/194 files (page 13/13)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 13 of 13. Use http://codebase.md/sooperset/mcp-atlassian?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   ├── devcontainer.json
│   ├── Dockerfile
│   ├── post-create.sh
│   └── post-start.sh
├── .dockerignore
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   ├── pull_request_template.md
│   └── workflows
│       ├── docker-publish.yml
│       ├── lint.yml
│       ├── publish.yml
│       ├── stale.yml
│       └── tests.yml
├── .gitignore
├── .pre-commit-config.yaml
├── AGENTS.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── pyproject.toml
├── README.md
├── scripts
│   ├── oauth_authorize.py
│   └── test_with_real_data.sh
├── SECURITY.md
├── smithery.yaml
├── src
│   └── mcp_atlassian
│       ├── __init__.py
│       ├── confluence
│       │   ├── __init__.py
│       │   ├── client.py
│       │   ├── comments.py
│       │   ├── config.py
│       │   ├── constants.py
│       │   ├── labels.py
│       │   ├── pages.py
│       │   ├── search.py
│       │   ├── spaces.py
│       │   ├── users.py
│       │   ├── utils.py
│       │   └── v2_adapter.py
│       ├── exceptions.py
│       ├── jira
│       │   ├── __init__.py
│       │   ├── attachments.py
│       │   ├── boards.py
│       │   ├── client.py
│       │   ├── comments.py
│       │   ├── config.py
│       │   ├── constants.py
│       │   ├── epics.py
│       │   ├── fields.py
│       │   ├── formatting.py
│       │   ├── issues.py
│       │   ├── links.py
│       │   ├── projects.py
│       │   ├── protocols.py
│       │   ├── search.py
│       │   ├── sprints.py
│       │   ├── transitions.py
│       │   ├── users.py
│       │   └── worklog.py
│       ├── models
│       │   ├── __init__.py
│       │   ├── base.py
│       │   ├── confluence
│       │   │   ├── __init__.py
│       │   │   ├── comment.py
│       │   │   ├── common.py
│       │   │   ├── label.py
│       │   │   ├── page.py
│       │   │   ├── search.py
│       │   │   ├── space.py
│       │   │   └── user_search.py
│       │   ├── constants.py
│       │   └── jira
│       │       ├── __init__.py
│       │       ├── agile.py
│       │       ├── comment.py
│       │       ├── common.py
│       │       ├── issue.py
│       │       ├── link.py
│       │       ├── project.py
│       │       ├── search.py
│       │       ├── version.py
│       │       ├── workflow.py
│       │       └── worklog.py
│       ├── preprocessing
│       │   ├── __init__.py
│       │   ├── base.py
│       │   ├── confluence.py
│       │   └── jira.py
│       ├── servers
│       │   ├── __init__.py
│       │   ├── confluence.py
│       │   ├── context.py
│       │   ├── dependencies.py
│       │   ├── jira.py
│       │   └── main.py
│       └── utils
│           ├── __init__.py
│           ├── date.py
│           ├── decorators.py
│           ├── env.py
│           ├── environment.py
│           ├── io.py
│           ├── lifecycle.py
│           ├── logging.py
│           ├── oauth_setup.py
│           ├── oauth.py
│           ├── ssl.py
│           ├── tools.py
│           └── urls.py
├── tests
│   ├── __init__.py
│   ├── conftest.py
│   ├── fixtures
│   │   ├── __init__.py
│   │   ├── confluence_mocks.py
│   │   └── jira_mocks.py
│   ├── integration
│   │   ├── conftest.py
│   │   ├── README.md
│   │   ├── test_authentication.py
│   │   ├── test_content_processing.py
│   │   ├── test_cross_service.py
│   │   ├── test_mcp_protocol.py
│   │   ├── test_proxy.py
│   │   ├── test_real_api.py
│   │   ├── test_ssl_verification.py
│   │   ├── test_stdin_monitoring_fix.py
│   │   └── test_transport_lifecycle.py
│   ├── README.md
│   ├── test_preprocessing.py
│   ├── test_real_api_validation.py
│   ├── unit
│   │   ├── confluence
│   │   │   ├── __init__.py
│   │   │   ├── conftest.py
│   │   │   ├── test_client_oauth.py
│   │   │   ├── test_client.py
│   │   │   ├── test_comments.py
│   │   │   ├── test_config.py
│   │   │   ├── test_constants.py
│   │   │   ├── test_custom_headers.py
│   │   │   ├── test_labels.py
│   │   │   ├── test_pages.py
│   │   │   ├── test_search.py
│   │   │   ├── test_spaces.py
│   │   │   ├── test_users.py
│   │   │   ├── test_utils.py
│   │   │   └── test_v2_adapter.py
│   │   ├── jira
│   │   │   ├── conftest.py
│   │   │   ├── test_attachments.py
│   │   │   ├── test_boards.py
│   │   │   ├── test_client_oauth.py
│   │   │   ├── test_client.py
│   │   │   ├── test_comments.py
│   │   │   ├── test_config.py
│   │   │   ├── test_constants.py
│   │   │   ├── test_custom_headers.py
│   │   │   ├── test_epics.py
│   │   │   ├── test_fields.py
│   │   │   ├── test_formatting.py
│   │   │   ├── test_issues_markdown.py
│   │   │   ├── test_issues.py
│   │   │   ├── test_links.py
│   │   │   ├── test_projects.py
│   │   │   ├── test_protocols.py
│   │   │   ├── test_search.py
│   │   │   ├── test_sprints.py
│   │   │   ├── test_transitions.py
│   │   │   ├── test_users.py
│   │   │   └── test_worklog.py
│   │   ├── models
│   │   │   ├── __init__.py
│   │   │   ├── conftest.py
│   │   │   ├── test_base_models.py
│   │   │   ├── test_confluence_models.py
│   │   │   ├── test_constants.py
│   │   │   └── test_jira_models.py
│   │   ├── servers
│   │   │   ├── __init__.py
│   │   │   ├── test_confluence_server.py
│   │   │   ├── test_context.py
│   │   │   ├── test_dependencies.py
│   │   │   ├── test_jira_server.py
│   │   │   └── test_main_server.py
│   │   ├── test_exceptions.py
│   │   ├── test_main_transport_selection.py
│   │   └── utils
│   │       ├── __init__.py
│   │       ├── test_custom_headers.py
│   │       ├── test_date.py
│   │       ├── test_decorators.py
│   │       ├── test_env.py
│   │       ├── test_environment.py
│   │       ├── test_io.py
│   │       ├── test_lifecycle.py
│   │       ├── test_logging.py
│   │       ├── test_masking.py
│   │       ├── test_oauth_setup.py
│   │       ├── test_oauth.py
│   │       ├── test_ssl.py
│   │       ├── test_tools.py
│   │       └── test_urls.py
│   └── utils
│       ├── __init__.py
│       ├── assertions.py
│       ├── base.py
│       ├── factories.py
│       └── mocks.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/tests/unit/jira/test_issues.py:
--------------------------------------------------------------------------------

```python
   1 | """Tests for the Jira Issues mixin."""
   2 | 
   3 | from unittest.mock import ANY, MagicMock, patch
   4 | 
   5 | import pytest
   6 | 
   7 | from mcp_atlassian.jira import JiraFetcher
   8 | from mcp_atlassian.jira.issues import IssuesMixin, logger
   9 | from mcp_atlassian.models.jira import JiraIssue
  10 | 
  11 | 
  12 | class TestIssuesMixin:
  13 |     """Tests for the IssuesMixin class."""
  14 | 
  15 |     @pytest.fixture
  16 |     def issues_mixin(self, jira_fetcher: JiraFetcher) -> IssuesMixin:
  17 |         """Create an IssuesMixin instance with mocked dependencies."""
  18 |         mixin = jira_fetcher
  19 | 
  20 |         # Add mock methods that would be provided by other mixins
  21 |         mixin._get_account_id = MagicMock(return_value="test-account-id")
  22 |         mixin.get_available_transitions = MagicMock(
  23 |             return_value=[{"id": "10", "name": "In Progress"}]
  24 |         )
  25 |         mixin.transition_issue = MagicMock(
  26 |             return_value=JiraIssue(id="123", key="TEST-123", summary="Test Issue")
  27 |         )
  28 | 
  29 |         return mixin
  30 | 
  31 |     def test_get_issue_basic(self, issues_mixin: IssuesMixin):
  32 |         """Test retrieving an issue by key."""
  33 |         # Mock the API response
  34 |         issues_mixin.jira.get_issue.return_value = {
  35 |             "id": "10001",
  36 |             "key": "TEST-123",
  37 |             "fields": {
  38 |                 "summary": "Test Issue",
  39 |                 "description": "This is a test issue",
  40 |                 "status": {"name": "Open"},
  41 |                 "issuetype": {"name": "Bug"},
  42 |                 "created": "2023-01-01T00:00:00.000+0000",
  43 |                 "updated": "2023-01-02T00:00:00.000+0000",
  44 |             },
  45 |         }
  46 | 
  47 |         # Call the method
  48 |         result = issues_mixin.get_issue("TEST-123")
  49 | 
  50 |         # Verify API calls
  51 |         issues_mixin.jira.get_issue.assert_called_once_with(
  52 |             "TEST-123",
  53 |             expand=None,
  54 |             fields=ANY,
  55 |             properties=None,
  56 |             update_history=True,
  57 |         )
  58 | 
  59 |         # Verify result structure
  60 |         assert isinstance(result, JiraIssue)
  61 |         assert result.key == "TEST-123"
  62 |         assert result.summary == "Test Issue"
  63 |         assert result.description == "This is a test issue"
  64 | 
  65 |         # Check Jira fields mapping
  66 |         assert result.status is not None
  67 |         assert result.status.name == "Open"
  68 |         assert result.issue_type.name == "Bug"
  69 | 
  70 |     def test_get_issue_with_comments(self, issues_mixin: IssuesMixin):
  71 |         """Test get_issue with comments."""
  72 |         # Mock the comments data
  73 |         comments_data = {
  74 |             "comments": [
  75 |                 {
  76 |                     "id": "1",
  77 |                     "body": "This is a comment",
  78 |                     "author": {"displayName": "John Doe"},
  79 |                     "created": "2023-01-02T00:00:00.000+0000",
  80 |                     "updated": "2023-01-02T00:00:00.000+0000",
  81 |                 }
  82 |             ]
  83 |         }
  84 | 
  85 |         # Mock the issue data
  86 |         issue_data = {
  87 |             "id": "12345",
  88 |             "key": "TEST-123",
  89 |             "fields": {
  90 |                 "comment": comments_data,
  91 |                 "summary": "Test Issue",
  92 |                 "description": "Test Description",
  93 |                 "status": {"name": "Open"},
  94 |                 "issuetype": {"name": "Bug"},
  95 |                 "created": "2023-01-01T00:00:00.000+0000",
  96 |                 "updated": "2023-01-02T00:00:00.000+0000",
  97 |             },
  98 |         }
  99 | 
 100 |         # Set up the mocked responses
 101 |         issues_mixin.jira.get_issue.return_value = issue_data
 102 |         issues_mixin.jira.issue_get_comments.return_value = comments_data
 103 | 
 104 |         # Call the method
 105 |         issue = issues_mixin.get_issue(
 106 |             "TEST-123",
 107 |             fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype,comment",
 108 |         )
 109 | 
 110 |         # Verify the API calls
 111 |         issues_mixin.jira.get_issue.assert_called_once_with(
 112 |             "TEST-123",
 113 |             expand=None,
 114 |             fields="summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype,comment",
 115 |             properties=None,
 116 |             update_history=True,
 117 |         )
 118 |         issues_mixin.jira.issue_get_comments.assert_called_once_with("TEST-123")
 119 | 
 120 |         # Verify the comments were added to the issue
 121 |         assert hasattr(issue, "comments")
 122 |         assert len(issue.comments) == 1
 123 |         assert issue.comments[0].body == "This is a comment"
 124 | 
 125 |     def test_get_issue_with_epic_info(self, issues_mixin: IssuesMixin):
 126 |         """Test retrieving issue with epic information."""
 127 |         try:
 128 |             # Mock the API responses for get_issue
 129 |             issues_mixin.jira.get_issue.side_effect = [
 130 |                 # First call - the issue
 131 |                 {
 132 |                     "id": "10001",
 133 |                     "key": "TEST-123",
 134 |                     "fields": {
 135 |                         "summary": "Test Issue",
 136 |                         "description": "This is a test issue",
 137 |                         "status": {"name": "Open"},
 138 |                         "issuetype": {"name": "Story"},
 139 |                         "customfield_10010": "EPIC-456",  # Epic Link field
 140 |                         "created": "2023-01-01T00:00:00.000+0000",
 141 |                         "updated": "2023-01-02T00:00:00.000+0000",
 142 |                     },
 143 |                 },
 144 |                 # Second call - the epic
 145 |                 {
 146 |                     "id": "10002",
 147 |                     "key": "EPIC-456",
 148 |                     "fields": {
 149 |                         "summary": "Epic Issue",
 150 |                         "description": "This is an epic",
 151 |                         "status": {"name": "In Progress"},
 152 |                         "issuetype": {"name": "Epic"},
 153 |                         "customfield_10011": "Epic Name Value",  # Epic Name field
 154 |                         "created": "2023-01-01T00:00:00.000+0000",
 155 |                         "updated": "2023-01-02T00:00:00.000+0000",
 156 |                     },
 157 |                 },
 158 |             ]
 159 | 
 160 |             # Mock get_field_ids_to_epic
 161 |             issues_mixin.get_field_ids_to_epic = MagicMock(
 162 |                 return_value={
 163 |                     "epic_link": "customfield_10010",
 164 |                     "epic_name": "customfield_10011",
 165 |                 }
 166 |             )
 167 | 
 168 |             # Call the method - just use get_issue without the include_epic_info parameter
 169 |             issue = issues_mixin.get_issue("TEST-123")
 170 | 
 171 |             # Verify the API calls
 172 |             issues_mixin.jira.get_issue.assert_any_call(
 173 |                 "TEST-123",
 174 |                 expand=None,
 175 |                 fields=ANY,
 176 |                 properties=None,
 177 |                 update_history=True,
 178 |             )
 179 |             issues_mixin.jira.get_issue.assert_any_call(
 180 |                 "EPIC-456",
 181 |                 expand=None,
 182 |                 fields=None,
 183 |                 properties=None,
 184 |                 update_history=True,
 185 |             )
 186 | 
 187 |             # Verify the issue
 188 |             assert issue.key == "TEST-123"
 189 |             assert issue.summary == "Test Issue"
 190 | 
 191 |             # Verify that the epic information is in the custom fields
 192 |             assert issue.custom_fields.get("customfield_10010") == {"value": "EPIC-456"}
 193 |             assert issue.custom_fields.get("customfield_10011") == {
 194 |                 "value": "Epic Name Value"
 195 |             }
 196 | 
 197 |         except Exception as e:
 198 |             pytest.fail(f"Test failed: {e}")
 199 | 
 200 |     def test_get_issue_error_handling(self, issues_mixin: IssuesMixin):
 201 |         """Test error handling in get_issue."""
 202 |         # Mock the API to raise an exception
 203 |         issues_mixin.jira.get_issue.side_effect = Exception("API error")
 204 | 
 205 |         # Call the method and verify it raises the expected exception
 206 |         with pytest.raises(
 207 |             Exception, match=r"Error retrieving issue TEST-123: API error"
 208 |         ):
 209 |             issues_mixin.get_issue("TEST-123")
 210 | 
 211 |     def test_normalize_comment_limit(self, issues_mixin: IssuesMixin):
 212 |         """Test normalizing comment limit."""
 213 |         # Test with None
 214 |         assert issues_mixin._normalize_comment_limit(None) is None
 215 | 
 216 |         # Test with integer
 217 |         assert issues_mixin._normalize_comment_limit(5) == 5
 218 | 
 219 |         # Test with "all"
 220 |         assert issues_mixin._normalize_comment_limit("all") is None
 221 | 
 222 |         # Test with string number
 223 |         assert issues_mixin._normalize_comment_limit("10") == 10
 224 | 
 225 |         # Test with invalid string
 226 |         assert issues_mixin._normalize_comment_limit("invalid") == 10
 227 | 
 228 |     def test_create_issue_basic(self, issues_mixin: IssuesMixin):
 229 |         """Test creating a basic issue."""
 230 |         # Mock create_issue response
 231 |         create_response = {"id": "12345", "key": "TEST-123"}
 232 |         issues_mixin.jira.create_issue.return_value = create_response
 233 | 
 234 |         # Mock the issue data for get_issue
 235 |         issue_data = {
 236 |             "id": "12345",
 237 |             "key": "TEST-123",
 238 |             "fields": {
 239 |                 "summary": "Test Issue",
 240 |                 "description": "This is a test issue",
 241 |                 "status": {"name": "Open"},
 242 |                 "issuetype": {"name": "Bug"},
 243 |             },
 244 |         }
 245 |         issues_mixin.jira.get_issue.return_value = issue_data
 246 | 
 247 |         # Mock empty comments
 248 |         issues_mixin.jira.issue_get_comments.return_value = {"comments": []}
 249 | 
 250 |         # Call create_issue
 251 |         issue = issues_mixin.create_issue(
 252 |             project_key="TEST",
 253 |             summary="Test Issue",
 254 |             issue_type="Bug",
 255 |             description="This is a test issue",
 256 |         )
 257 | 
 258 |         # Verify API calls
 259 |         expected_fields = {
 260 |             "project": {"key": "TEST"},
 261 |             "summary": "Test Issue",
 262 |             "issuetype": {"name": "Bug"},
 263 |             "description": "This is a test issue",
 264 |         }
 265 |         issues_mixin.jira.create_issue.assert_called_once_with(fields=expected_fields)
 266 |         issues_mixin.jira.get_issue.assert_called_once_with("TEST-123")
 267 | 
 268 |         # Verify issue
 269 |         assert issue.key == "TEST-123"
 270 |         assert issue.summary == "Test Issue"
 271 | 
 272 |     def test_create_issue_no_components(self, issues_mixin: IssuesMixin):
 273 |         """Test creating an issue with no components specified."""
 274 |         # Mock create_issue response
 275 |         create_response = {"id": "12345", "key": "TEST-123"}
 276 |         issues_mixin.jira.create_issue.return_value = create_response
 277 | 
 278 |         # Mock the issue data for get_issue
 279 |         issue_data = {
 280 |             "id": "12345",
 281 |             "key": "TEST-123",
 282 |             "fields": {
 283 |                 "summary": "Test Issue",
 284 |                 "description": "This is a test issue",
 285 |                 "status": {"name": "Open"},
 286 |                 "issuetype": {"name": "Bug"},
 287 |             },
 288 |         }
 289 |         issues_mixin.jira.get_issue.return_value = issue_data
 290 | 
 291 |         # Mock empty comments
 292 |         issues_mixin.jira.issue_get_comments.return_value = {"comments": []}
 293 | 
 294 |         # Call create_issue with components=None
 295 |         issue = issues_mixin.create_issue(
 296 |             project_key="TEST",
 297 |             summary="Test Issue",
 298 |             issue_type="Bug",
 299 |             description="This is a test issue",
 300 |             components=None,
 301 |         )
 302 | 
 303 |         # Verify API calls
 304 |         expected_fields = {
 305 |             "project": {"key": "TEST"},
 306 |             "summary": "Test Issue",
 307 |             "issuetype": {"name": "Bug"},
 308 |             "description": "This is a test issue",
 309 |         }
 310 |         issues_mixin.jira.create_issue.assert_called_once_with(fields=expected_fields)
 311 | 
 312 |         # Verify 'components' is not in the fields
 313 |         assert "components" not in issues_mixin.jira.create_issue.call_args[1]["fields"]
 314 | 
 315 |     def test_create_issue_single_component(self, issues_mixin: IssuesMixin):
 316 |         """Test creating an issue with a single component."""
 317 |         # Mock create_issue response
 318 |         create_response = {"id": "12345", "key": "TEST-123"}
 319 |         issues_mixin.jira.create_issue.return_value = create_response
 320 | 
 321 |         # Mock the issue data for get_issue
 322 |         issue_data = {
 323 |             "id": "12345",
 324 |             "key": "TEST-123",
 325 |             "fields": {
 326 |                 "summary": "Test Issue",
 327 |                 "description": "This is a test issue",
 328 |                 "status": {"name": "Open"},
 329 |                 "issuetype": {"name": "Bug"},
 330 |                 "components": [{"name": "UI"}],
 331 |             },
 332 |         }
 333 |         issues_mixin.jira.get_issue.return_value = issue_data
 334 | 
 335 |         # Mock empty comments
 336 |         issues_mixin.jira.issue_get_comments.return_value = {"comments": []}
 337 | 
 338 |         # Call create_issue with a single component
 339 |         issue = issues_mixin.create_issue(
 340 |             project_key="TEST",
 341 |             summary="Test Issue",
 342 |             issue_type="Bug",
 343 |             description="This is a test issue",
 344 |             components=["UI"],
 345 |         )
 346 | 
 347 |         # Verify API calls
 348 |         expected_fields = {
 349 |             "project": {"key": "TEST"},
 350 |             "summary": "Test Issue",
 351 |             "issuetype": {"name": "Bug"},
 352 |             "description": "This is a test issue",
 353 |             "components": [{"name": "UI"}],
 354 |         }
 355 |         issues_mixin.jira.create_issue.assert_called_once_with(fields=expected_fields)
 356 | 
 357 |         # Verify the components field was passed correctly
 358 |         assert issues_mixin.jira.create_issue.call_args[1]["fields"]["components"] == [
 359 |             {"name": "UI"}
 360 |         ]
 361 | 
 362 |     def test_create_issue_multiple_components(self, issues_mixin: IssuesMixin):
 363 |         """Test creating an issue with multiple components."""
 364 |         # Mock create_issue response
 365 |         create_response = {"id": "12345", "key": "TEST-123"}
 366 |         issues_mixin.jira.create_issue.return_value = create_response
 367 | 
 368 |         # Mock the issue data for get_issue
 369 |         issue_data = {
 370 |             "id": "12345",
 371 |             "key": "TEST-123",
 372 |             "fields": {
 373 |                 "summary": "Test Issue",
 374 |                 "description": "This is a test issue",
 375 |                 "status": {"name": "Open"},
 376 |                 "issuetype": {"name": "Bug"},
 377 |                 "components": [{"name": "UI"}, {"name": "API"}],
 378 |             },
 379 |         }
 380 |         issues_mixin.jira.get_issue.return_value = issue_data
 381 | 
 382 |         # Mock empty comments
 383 |         issues_mixin.jira.issue_get_comments.return_value = {"comments": []}
 384 | 
 385 |         # Call create_issue with multiple components
 386 |         issue = issues_mixin.create_issue(
 387 |             project_key="TEST",
 388 |             summary="Test Issue",
 389 |             issue_type="Bug",
 390 |             description="This is a test issue",
 391 |             components=["UI", "API"],
 392 |         )
 393 | 
 394 |         # Verify API calls
 395 |         expected_fields = {
 396 |             "project": {"key": "TEST"},
 397 |             "summary": "Test Issue",
 398 |             "issuetype": {"name": "Bug"},
 399 |             "description": "This is a test issue",
 400 |             "components": [{"name": "UI"}, {"name": "API"}],
 401 |         }
 402 |         issues_mixin.jira.create_issue.assert_called_once_with(fields=expected_fields)
 403 | 
 404 |         # Verify the components field was passed correctly
 405 |         assert issues_mixin.jira.create_issue.call_args[1]["fields"]["components"] == [
 406 |             {"name": "UI"},
 407 |             {"name": "API"},
 408 |         ]
 409 | 
 410 |     def test_create_issue_components_with_invalid_entries(
 411 |         self, issues_mixin: IssuesMixin
 412 |     ):
 413 |         """Test creating an issue with components list containing invalid entries."""
 414 |         # Mock create_issue response
 415 |         create_response = {"id": "12345", "key": "TEST-123"}
 416 |         issues_mixin.jira.create_issue.return_value = create_response
 417 | 
 418 |         # Mock the issue data for get_issue
 419 |         issue_data = {
 420 |             "id": "12345",
 421 |             "key": "TEST-123",
 422 |             "fields": {
 423 |                 "summary": "Test Issue",
 424 |                 "description": "This is a test issue",
 425 |                 "status": {"name": "Open"},
 426 |                 "issuetype": {"name": "Bug"},
 427 |                 "components": [{"name": "Valid"}, {"name": "Backend"}],
 428 |             },
 429 |         }
 430 |         issues_mixin.jira.get_issue.return_value = issue_data
 431 | 
 432 |         # Mock empty comments
 433 |         issues_mixin.jira.issue_get_comments.return_value = {"comments": []}
 434 | 
 435 |         # Call create_issue with components list containing invalid entries
 436 |         issue = issues_mixin.create_issue(
 437 |             project_key="TEST",
 438 |             summary="Test Issue",
 439 |             issue_type="Bug",
 440 |             description="This is a test issue",
 441 |             components=["Valid", "", None, "  Backend  "],
 442 |         )
 443 | 
 444 |         # Verify API calls
 445 |         expected_fields = {
 446 |             "project": {"key": "TEST"},
 447 |             "summary": "Test Issue",
 448 |             "issuetype": {"name": "Bug"},
 449 |             "description": "This is a test issue",
 450 |             "components": [{"name": "Valid"}, {"name": "Backend"}],
 451 |         }
 452 |         issues_mixin.jira.create_issue.assert_called_once_with(fields=expected_fields)
 453 | 
 454 |         # Verify the components field was passed correctly, with invalid entries filtered out
 455 |         assert issues_mixin.jira.create_issue.call_args[1]["fields"]["components"] == [
 456 |             {"name": "Valid"},
 457 |             {"name": "Backend"},
 458 |         ]
 459 | 
 460 |     def test_create_issue_components_precedence(self, issues_mixin, caplog):
 461 |         """Test that explicit components take precedence over components in additional_fields."""
 462 |         # Mock create_issue response
 463 |         create_response = {"id": "12345", "key": "TEST-123"}
 464 |         issues_mixin.jira.create_issue.return_value = create_response
 465 | 
 466 |         # Mock the issue data for get_issue
 467 |         issue_data = {
 468 |             "id": "12345",
 469 |             "key": "TEST-123",
 470 |             "fields": {
 471 |                 "summary": "Test Issue",
 472 |                 "description": "This is a test issue",
 473 |                 "status": {"name": "Open"},
 474 |                 "issuetype": {"name": "Bug"},
 475 |                 "components": [{"name": "Explicit"}],
 476 |             },
 477 |         }
 478 |         issues_mixin.jira.get_issue.return_value = issue_data
 479 | 
 480 |         # Mock empty comments
 481 |         issues_mixin.jira.issue_get_comments.return_value = {"comments": []}
 482 | 
 483 |         # Direct test for the precedence handling logic
 484 |         # Create fields dict with components already set by explicit parameter
 485 |         fields = {
 486 |             "project": {"key": "TEST"},
 487 |             "summary": "Test Issue",
 488 |             "issuetype": {"name": "Bug"},
 489 |             "description": "This is a test issue",
 490 |             "components": [{"name": "Explicit"}],
 491 |         }
 492 | 
 493 |         # Create kwargs with a conflicting components entry
 494 |         kwargs = {"components": [{"name": "Ignored"}]}
 495 | 
 496 |         # Directly call the method that would handle the precedence
 497 |         # This simulates what happens inside create_issue
 498 |         if "components" in fields and "components" in kwargs:
 499 |             logger.warning(
 500 |                 "Components provided via both 'components' argument and 'additional_fields'. "
 501 |                 "Using the explicit 'components' argument."
 502 |             )
 503 |             # Remove the conflicting key from kwargs to prevent issues later
 504 |             kwargs.pop("components", None)
 505 | 
 506 |         # Verify the warning was logged about the conflict
 507 |         assert (
 508 |             "Components provided via both 'components' argument and 'additional_fields'"
 509 |             in caplog.text
 510 |         )
 511 | 
 512 |         # Verify that kwargs no longer contains components
 513 |         assert "components" not in kwargs
 514 | 
 515 |         # Verify the components field was preserved with the explicit value
 516 |         assert fields["components"] == [{"name": "Explicit"}]
 517 | 
 518 |     def test_create_issue_with_assignee_cloud(self, issues_mixin: IssuesMixin):
 519 |         """Test creating an issue with an assignee in Jira Cloud."""
 520 |         # Mock create_issue response
 521 |         create_response = {"key": "TEST-123"}
 522 |         issues_mixin.jira.create_issue.return_value = create_response
 523 | 
 524 |         # Mock get_issue response
 525 |         issues_mixin.get_issue = MagicMock(
 526 |             return_value=JiraIssue(key="TEST-123", description="", summary="Test Issue")
 527 |         )
 528 | 
 529 |         # Mock _get_account_id to return a Cloud account ID
 530 |         issues_mixin._get_account_id = MagicMock(return_value="cloud-account-id")
 531 | 
 532 |         # Configure for Cloud
 533 |         issues_mixin.config = MagicMock()
 534 |         issues_mixin.config.is_cloud = True
 535 | 
 536 |         # Call the method
 537 |         issues_mixin.create_issue(
 538 |             project_key="TEST",
 539 |             summary="Test Issue",
 540 |             issue_type="Bug",
 541 |             assignee="testuser",
 542 |         )
 543 | 
 544 |         # Verify _get_account_id was called with the correct username
 545 |         issues_mixin._get_account_id.assert_called_once_with("testuser")
 546 | 
 547 |         # Verify the assignee was properly set for Cloud (accountId)
 548 |         fields = issues_mixin.jira.create_issue.call_args[1]["fields"]
 549 |         assert fields["assignee"] == {"accountId": "cloud-account-id"}
 550 | 
 551 |     def test_create_issue_with_assignee_server(self, issues_mixin: IssuesMixin):
 552 |         """Test creating an issue with an assignee in Jira Server/DC."""
 553 |         # Mock create_issue response
 554 |         create_response = {"key": "TEST-456"}
 555 |         issues_mixin.jira.create_issue.return_value = create_response
 556 | 
 557 |         # Mock get_issue response
 558 |         issues_mixin.get_issue = MagicMock(
 559 |             return_value=JiraIssue(key="TEST-456", description="", summary="Test Issue")
 560 |         )
 561 | 
 562 |         # Mock _get_account_id to return a Server user ID (typically username)
 563 |         issues_mixin._get_account_id = MagicMock(return_value="server-user")
 564 | 
 565 |         # Configure for Server/DC
 566 |         issues_mixin.config = MagicMock()
 567 |         issues_mixin.config.is_cloud = False
 568 | 
 569 |         # Call the method
 570 |         issues_mixin.create_issue(
 571 |             project_key="TEST",
 572 |             summary="Test Issue",
 573 |             issue_type="Bug",
 574 |             assignee="testuser",
 575 |         )
 576 | 
 577 |         # Verify _get_account_id was called with the correct username
 578 |         issues_mixin._get_account_id.assert_called_once_with("testuser")
 579 | 
 580 |         # Verify the assignee was properly set for Server/DC (name)
 581 |         fields = issues_mixin.jira.create_issue.call_args[1]["fields"]
 582 |         assert fields["assignee"] == {"name": "server-user"}
 583 | 
 584 |     def test_create_epic(self, issues_mixin: IssuesMixin):
 585 |         """Test creating an epic."""
 586 |         # Mock responses
 587 |         create_response = {"key": "EPIC-123"}
 588 |         issues_mixin.jira.create_issue.return_value = create_response
 589 |         issues_mixin.get_issue = MagicMock(
 590 |             return_value=JiraIssue(key="EPIC-123", description="", summary="Test Epic")
 591 |         )
 592 | 
 593 |         # Mock the prepare_epic_fields method from EpicsMixin
 594 |         with patch(
 595 |             "mcp_atlassian.jira.epics.EpicsMixin.prepare_epic_fields", autospec=True
 596 |         ) as mock_prepare_epic:
 597 |             # Set up the mock to store epic values in kwargs
 598 |             # Note: First argument is self because EpicsMixin.prepare_epic_fields is called as a class method
 599 |             def side_effect(self_args, fields, summary, kwargs, project_key):
 600 |                 kwargs["__epic_name_value"] = summary
 601 |                 kwargs["__epic_name_field"] = "customfield_10011"
 602 |                 return None
 603 | 
 604 |             mock_prepare_epic.side_effect = side_effect
 605 | 
 606 |             # Mock get_field_ids_to_epic
 607 |             with patch.object(
 608 |                 issues_mixin,
 609 |                 "get_field_ids_to_epic",
 610 |                 return_value={"Epic Name": "customfield_10011"},
 611 |             ):
 612 |                 # Call the method
 613 |                 result = issues_mixin.create_issue(
 614 |                     project_key="TEST",
 615 |                     summary="Test Epic",
 616 |                     issue_type="Epic",
 617 |                 )
 618 | 
 619 |                 # Verify create_issue was called with the right project and summary
 620 |                 create_args = issues_mixin.jira.create_issue.call_args[1]
 621 |                 fields = create_args["fields"]
 622 |                 assert fields["project"]["key"] == "TEST"
 623 |                 assert fields["summary"] == "Test Epic"
 624 | 
 625 |                 # Verify epic fields are NOT in the fields dictionary (two-step creation)
 626 |                 assert "customfield_10011" not in fields
 627 | 
 628 |                 # Verify that prepare_epic_fields was called
 629 |                 mock_prepare_epic.assert_called_once()
 630 | 
 631 |                 # For an Epic, verify that update_issue should be called for the second step
 632 |                 # This would happen in the EpicsMixin.update_epic_fields method which is called
 633 |                 # after the initial creation
 634 |                 assert issues_mixin.get_issue.called
 635 |                 assert result.key == "EPIC-123"
 636 | 
 637 |     def test_update_issue_basic(self, issues_mixin: IssuesMixin):
 638 |         """Test updating an issue with basic fields."""
 639 |         # Mock the issue data for get_issue
 640 |         issue_data = {
 641 |             "id": "12345",
 642 |             "key": "TEST-123",
 643 |             "fields": {
 644 |                 "summary": "Updated Summary",
 645 |                 "description": "This is a test issue",
 646 |                 "status": {"name": "In Progress"},
 647 |                 "issuetype": {"name": "Bug"},
 648 |             },
 649 |         }
 650 |         issues_mixin.jira.get_issue.return_value = issue_data
 651 | 
 652 |         # Mock empty comments
 653 |         issues_mixin.jira.issue_get_comments.return_value = {"comments": []}
 654 | 
 655 |         # Call the method
 656 |         document = issues_mixin.update_issue(
 657 |             issue_key="TEST-123", fields={"summary": "Updated Summary"}
 658 |         )
 659 | 
 660 |         # Verify the API calls
 661 |         issues_mixin.jira.update_issue.assert_called_once_with(
 662 |             issue_key="TEST-123", update={"fields": {"summary": "Updated Summary"}}
 663 |         )
 664 |         assert issues_mixin.jira.get_issue.called
 665 |         assert issues_mixin.jira.get_issue.call_args[0][0] == "TEST-123"
 666 | 
 667 |         # Verify the result
 668 |         assert document.id == "12345"
 669 |         assert document.key == "TEST-123"
 670 |         assert document.summary == "Updated Summary"
 671 | 
 672 |     def test_update_issue_with_status(self, issues_mixin: IssuesMixin):
 673 |         """Test updating an issue with a status change."""
 674 |         # Mock get_issue response
 675 |         issues_mixin.get_issue = MagicMock(
 676 |             return_value=JiraIssue(key="TEST-123", description="")
 677 |         )
 678 | 
 679 |         # Mock available transitions (using TransitionsMixin's normalized format)
 680 |         issues_mixin.get_available_transitions = MagicMock(
 681 |             return_value=[
 682 |                 {
 683 |                     "id": "21",
 684 |                     "name": "In Progress",
 685 |                     "to_status": "In Progress",
 686 |                 }
 687 |             ]
 688 |         )
 689 | 
 690 |         # Call the method with status in kwargs instead of fields
 691 |         issues_mixin.update_issue(issue_key="TEST-123", status="In Progress")
 692 | 
 693 |     def test_update_issue_unassign(self, issues_mixin: IssuesMixin):
 694 |         """Test unassigning an issue."""
 695 |         issue_data = {
 696 |             "id": "12345",
 697 |             "key": "TEST-123",
 698 |             "fields": {
 699 |                 "summary": "Test Issue",
 700 |                 "description": "This is a test",
 701 |                 "status": {"name": "Open"},
 702 |                 "issuetype": {"name": "Bug"},
 703 |             },
 704 |         }
 705 |         issues_mixin.jira.get_issue.return_value = issue_data
 706 |         issues_mixin.jira.issue_get_comments.return_value = {"comments": []}
 707 |         issues_mixin._get_account_id = MagicMock()
 708 | 
 709 |         document = issues_mixin.update_issue(issue_key="TEST-123", assignee=None)
 710 | 
 711 |         issues_mixin.jira.update_issue.assert_called_once_with(
 712 |             issue_key="TEST-123", update={"fields": {"assignee": None}}
 713 |         )
 714 |         assert not issues_mixin._get_account_id.called
 715 |         assert document.key == "TEST-123"
 716 | 
 717 |     def test_delete_issue(self, issues_mixin: IssuesMixin):
 718 |         """Test deleting an issue."""
 719 |         # Call the method
 720 |         result = issues_mixin.delete_issue("TEST-123")
 721 | 
 722 |         # Verify the API call
 723 |         issues_mixin.jira.delete_issue.assert_called_once_with("TEST-123")
 724 |         assert result is True
 725 | 
 726 |     def test_delete_issue_error(self, issues_mixin: IssuesMixin):
 727 |         """Test error handling when deleting an issue."""
 728 |         # Setup mock to throw exception
 729 |         issues_mixin.jira.delete_issue.side_effect = Exception("Delete failed")
 730 | 
 731 |         # Call the method and verify exception is raised correctly
 732 |         with pytest.raises(
 733 |             Exception, match="Error deleting issue TEST-123: Delete failed"
 734 |         ):
 735 |             issues_mixin.delete_issue("TEST-123")
 736 | 
 737 |     def test_process_additional_fields_with_fixversions(
 738 |         self, issues_mixin: IssuesMixin
 739 |     ):
 740 |         """Test _process_additional_fields properly handles fixVersions field."""
 741 |         # Initialize test data
 742 |         fields = {}
 743 |         kwargs = {"fixVersions": [{"name": "TestRelease"}]}
 744 | 
 745 |         # Call the method
 746 |         issues_mixin._process_additional_fields(fields, kwargs)
 747 | 
 748 |         # Verify fixVersions was added correctly to fields
 749 |         assert "fixVersions" in fields
 750 |         assert fields["fixVersions"] == [{"name": "TestRelease"}]
 751 | 
 752 |     def test_create_issue_with_parent_for_task(self, issues_mixin: IssuesMixin):
 753 |         """Test creating a regular task issue with a parent field."""
 754 |         # Setup mock response for create_issue
 755 |         create_response = {
 756 |             "id": "12345",
 757 |             "key": "TEST-456",
 758 |             "self": "https://jira.example.com/rest/api/2/issue/12345",
 759 |         }
 760 |         issues_mixin.jira.create_issue.return_value = create_response
 761 | 
 762 |         # Setup mock response for issue retrieval
 763 |         issue_response = {
 764 |             "id": "12345",
 765 |             "key": "TEST-456",
 766 |             "fields": {
 767 |                 "summary": "Test Task with Parent",
 768 |                 "description": "This is a test",
 769 |                 "status": {"name": "Open"},
 770 |                 "issuetype": {"name": "Task"},
 771 |                 "parent": {"key": "TEST-123"},
 772 |             },
 773 |         }
 774 |         issues_mixin.jira.get_issue.return_value = issue_response
 775 | 
 776 |         issues_mixin._get_account_id = MagicMock(return_value="user123")
 777 | 
 778 |         # Execute - create a Task with parent field
 779 |         result = issues_mixin.create_issue(
 780 |             project_key="TEST",
 781 |             summary="Test Task with Parent",
 782 |             issue_type="Task",
 783 |             description="This is a test",
 784 |             assignee="jdoe",
 785 |             parent="TEST-123",  # Adding parent for a non-subtask
 786 |         )
 787 | 
 788 |         # Verify
 789 |         issues_mixin.jira.create_issue.assert_called_once()
 790 |         call_kwargs = issues_mixin.jira.create_issue.call_args[1]
 791 |         assert "fields" in call_kwargs
 792 |         fields = call_kwargs["fields"]
 793 | 
 794 |         # Verify parent field was included
 795 |         assert "parent" in fields
 796 |         assert fields["parent"] == {"key": "TEST-123"}
 797 | 
 798 |         # Verify issue method was called after creation
 799 |         assert issues_mixin.jira.get_issue.called
 800 |         assert issues_mixin.jira.get_issue.call_args[0][0] == "TEST-456"
 801 | 
 802 |         # Verify the issue was created successfully
 803 |         assert result is not None
 804 |         assert result.key == "TEST-456"
 805 | 
 806 |     def test_create_issue_with_fixversions(self, issues_mixin: IssuesMixin):
 807 |         """Test creating an issue with fixVersions in additional_fields."""
 808 |         # Mock create_issue response
 809 |         create_response = {"id": "12345", "key": "TEST-123"}
 810 |         issues_mixin.jira.create_issue.return_value = create_response
 811 | 
 812 |         # Mock the issue data for get_issue
 813 |         issue_data = {
 814 |             "id": "12345",
 815 |             "key": "TEST-123",
 816 |             "fields": {
 817 |                 "summary": "Test Issue",
 818 |                 "description": "This is a test issue",
 819 |                 "status": {"name": "Open"},
 820 |                 "issuetype": {"name": "Bug"},
 821 |                 "fixVersions": [{"name": "1.0.0"}],
 822 |             },
 823 |         }
 824 |         issues_mixin.jira.get_issue.return_value = issue_data
 825 | 
 826 |         # Create the issue with fixVersions in additional_fields
 827 |         result = issues_mixin.create_issue(
 828 |             project_key="TEST",
 829 |             summary="Test Issue",
 830 |             issue_type="Bug",
 831 |             description="This is a test issue",
 832 |             fixVersions=[{"name": "1.0.0"}],
 833 |         )
 834 | 
 835 |         # Verify API call to create issue
 836 |         issues_mixin.jira.create_issue.assert_called_once()
 837 |         call_args = issues_mixin.jira.create_issue.call_args[1]
 838 |         fields = call_args["fields"]
 839 |         assert fields["project"]["key"] == "TEST"
 840 |         assert fields["summary"] == "Test Issue"
 841 |         assert fields["issuetype"]["name"] == "Bug"
 842 |         assert fields["description"] == "This is a test issue"
 843 |         assert "fixVersions" in fields
 844 |         assert fields["fixVersions"] == [{"name": "1.0.0"}]
 845 | 
 846 |         # Verify API call to get issue
 847 |         issues_mixin.jira.get_issue.assert_called_once_with("TEST-123")
 848 | 
 849 |         # Verify result
 850 |         assert result.key == "TEST-123"
 851 |         assert result.summary == "Test Issue"
 852 |         assert result.issue_type and result.issue_type.name == "Bug"
 853 |         assert hasattr(result, "fix_versions")
 854 |         assert len(result.fix_versions) == 1
 855 |         # The JiraIssue model might process fixVersions differently, check the actual structure
 856 |         # This depends on how JiraIssue.from_api_response handles the fixVersions field
 857 |         # If it's a list of dictionaries, use:
 858 |         if hasattr(result.fix_versions[0], "name"):
 859 |             assert result.fix_versions[0].name == "1.0.0"
 860 |         else:
 861 |             # If it's a list of strings or other format, adjust accordingly:
 862 |             assert "1.0.0" in str(result.fix_versions[0])
 863 | 
 864 |     def test_get_issue_with_custom_fields(self, issues_mixin: IssuesMixin):
 865 |         """Test get_issue with custom fields parameter."""
 866 |         # Mock the response with custom fields
 867 |         mock_issue = {
 868 |             "id": "10001",
 869 |             "key": "TEST-123",
 870 |             "fields": {
 871 |                 "summary": "Test issue with custom field",
 872 |                 "customfield_10049": "Custom value",
 873 |                 "customfield_10050": {"value": "Option value"},
 874 |                 "description": "Issue description",
 875 |             },
 876 |         }
 877 |         issues_mixin.jira.get_issue.return_value = mock_issue
 878 | 
 879 |         # Test with string format
 880 |         issue = issues_mixin.get_issue("TEST-123", fields="summary,customfield_10049")
 881 | 
 882 |         # Verify the API call
 883 |         issues_mixin.jira.get_issue.assert_called_with(
 884 |             "TEST-123",
 885 |             expand=None,
 886 |             fields="summary,customfield_10049",
 887 |             properties=None,
 888 |             update_history=True,
 889 |         )
 890 | 
 891 |         # Check the result
 892 |         simplified = issue.to_simplified_dict()
 893 |         assert "customfield_10049" in simplified
 894 |         assert simplified["customfield_10049"] == {"value": "Custom value"}
 895 |         assert "description" not in simplified
 896 | 
 897 |         # Test with list format
 898 |         issues_mixin.jira.get_issue.reset_mock()
 899 |         issue = issues_mixin.get_issue(
 900 |             "TEST-123", fields=["summary", "customfield_10050"]
 901 |         )
 902 | 
 903 |         # Verify API call converts list to comma-separated string
 904 |         issues_mixin.jira.get_issue.assert_called_with(
 905 |             "TEST-123",
 906 |             expand=None,
 907 |             fields="summary,customfield_10050",
 908 |             properties=None,
 909 |             update_history=True,
 910 |         )
 911 | 
 912 |         # Check the result
 913 |         simplified = issue.to_simplified_dict()
 914 |         assert "customfield_10050" in simplified
 915 |         assert simplified["customfield_10050"] == {"value": "Option value"}
 916 | 
 917 |     def test_get_issue_with_all_fields(self, issues_mixin: IssuesMixin):
 918 |         """Test get_issue with '*all' fields parameter."""
 919 |         # Mock the response
 920 |         mock_issue = {
 921 |             "id": "10001",
 922 |             "key": "TEST-123",
 923 |             "fields": {
 924 |                 "summary": "Test issue",
 925 |                 "description": "Description",
 926 |                 "customfield_10049": "Custom value",
 927 |             },
 928 |         }
 929 |         issues_mixin.jira.get_issue.return_value = mock_issue
 930 | 
 931 |         # Test with "*all" parameter
 932 |         issue = issues_mixin.get_issue("TEST-123", fields="*all")
 933 | 
 934 |         # Check that all fields are included
 935 |         simplified = issue.to_simplified_dict()
 936 |         assert "summary" in simplified
 937 |         assert "description" in simplified
 938 |         assert "customfield_10049" in simplified
 939 | 
 940 |     def test_get_issue_with_properties(self, issues_mixin: IssuesMixin):
 941 |         """Test get_issue with properties parameter."""
 942 |         # Mock the response
 943 |         issues_mixin.jira.get_issue.return_value = {
 944 |             "id": "10001",
 945 |             "key": "TEST-123",
 946 |             "fields": {},
 947 |         }
 948 | 
 949 |         # Test with properties parameter as string
 950 |         issues_mixin.get_issue("TEST-123", properties="property1,property2")
 951 | 
 952 |         # Verify API call - should include properties parameter and add 'properties' to fields
 953 |         issues_mixin.jira.get_issue.assert_called_with(
 954 |             "TEST-123",
 955 |             expand=None,
 956 |             fields=ANY,
 957 |             properties="property1,property2",
 958 |             update_history=True,
 959 |         )
 960 | 
 961 |         # Test with properties parameter as list
 962 |         issues_mixin.jira.get_issue.reset_mock()
 963 |         issues_mixin.get_issue("TEST-123", properties=["property1", "property2"])
 964 | 
 965 |         # Verify API call - should include properties parameter as comma-separated string and add 'properties' to fields
 966 |         issues_mixin.jira.get_issue.assert_called_with(
 967 |             "TEST-123",
 968 |             expand=None,
 969 |             fields=ANY,
 970 |             properties="property1,property2",
 971 |             update_history=True,
 972 |         )
 973 | 
 974 |     def test_get_issue_with_update_history(self, issues_mixin: IssuesMixin):
 975 |         """Test get_issue with update_history parameter."""
 976 |         # Mock the response
 977 |         issues_mixin.jira.get_issue.return_value = {
 978 |             "id": "10001",
 979 |             "key": "TEST-123",
 980 |             "fields": {},
 981 |         }
 982 | 
 983 |         # Test with update_history=False
 984 |         issues_mixin.get_issue("TEST-123", update_history=False)
 985 | 
 986 |         # Verify API call - should include update_history parameter
 987 |         issues_mixin.jira.get_issue.assert_called_with(
 988 |             "TEST-123",
 989 |             expand=None,
 990 |             fields=ANY,
 991 |             properties=None,
 992 |             update_history=False,
 993 |         )
 994 | 
 995 |     def test_batch_create_issues_basic(self, issues_mixin: IssuesMixin):
 996 |         """Test basic functionality of batch_create_issues."""
 997 |         # Setup test data
 998 |         issues = [
 999 |             {
1000 |                 "project_key": "TEST",
1001 |                 "summary": "Test Issue 1",
1002 |                 "issue_type": "Task",
1003 |                 "description": "Description 1",
1004 |             },
1005 |             {
1006 |                 "project_key": "TEST",
1007 |                 "summary": "Test Issue 2",
1008 |                 "issue_type": "Bug",
1009 |                 "description": "Description 2",
1010 |                 "assignee": "john.doe",
1011 |                 "components": ["Frontend"],
1012 |             },
1013 |         ]
1014 | 
1015 |         # Mock bulk create response
1016 |         bulk_response = {
1017 |             "issues": [
1018 |                 {"id": "1", "key": "TEST-1", "self": "http://example.com/TEST-1"},
1019 |                 {"id": "2", "key": "TEST-2", "self": "http://example.com/TEST-2"},
1020 |             ],
1021 |             "errors": [],
1022 |         }
1023 |         issues_mixin.jira.create_issues.return_value = bulk_response
1024 | 
1025 |         # Mock get_issue responses
1026 |         def get_issue_side_effect(key):
1027 |             if key == "TEST-1":
1028 |                 return {
1029 |                     "id": "1",
1030 |                     "key": "TEST-1",
1031 |                     "fields": {"summary": "Test Issue 1"},
1032 |                 }
1033 |             return {"id": "2", "key": "TEST-2", "fields": {"summary": "Test Issue 2"}}
1034 | 
1035 |         issues_mixin.jira.get_issue.side_effect = get_issue_side_effect
1036 |         issues_mixin._get_account_id.return_value = "user123"
1037 | 
1038 |         # Call the method
1039 |         result = issues_mixin.batch_create_issues(issues)
1040 | 
1041 |         # Verify results
1042 |         assert len(result) == 2
1043 |         assert result[0].key == "TEST-1"
1044 |         assert result[1].key == "TEST-2"
1045 | 
1046 |         # Verify bulk create was called correctly
1047 |         issues_mixin.jira.create_issues.assert_called_once()
1048 |         call_args = issues_mixin.jira.create_issues.call_args[0][0]
1049 |         assert len(call_args) == 2
1050 |         assert call_args[0]["fields"]["summary"] == "Test Issue 1"
1051 |         assert call_args[1]["fields"]["summary"] == "Test Issue 2"
1052 | 
1053 |     def test_batch_create_issues_validate_only(self, issues_mixin: IssuesMixin):
1054 |         """Test batch_create_issues with validate_only=True."""
1055 |         # Setup test data
1056 |         issues = [
1057 |             {
1058 |                 "project_key": "TEST",
1059 |                 "summary": "Test Issue 1",
1060 |                 "issue_type": "Task",
1061 |             },
1062 |             {
1063 |                 "project_key": "TEST",
1064 |                 "summary": "Test Issue 2",
1065 |                 "issue_type": "Bug",
1066 |             },
1067 |         ]
1068 | 
1069 |         # Call the method with validate_only=True
1070 |         result = issues_mixin.batch_create_issues(issues, validate_only=True)
1071 | 
1072 |         # Verify no issues were created
1073 |         assert len(result) == 0
1074 |         assert not issues_mixin.jira.create_issues.called
1075 | 
1076 |     def test_batch_create_issues_missing_required_fields(
1077 |         self, issues_mixin: IssuesMixin
1078 |     ):
1079 |         """Test batch_create_issues with missing required fields."""
1080 |         # Setup test data with missing fields
1081 |         issues = [
1082 |             {
1083 |                 "project_key": "TEST",
1084 |                 "summary": "Test Issue 1",
1085 |                 # Missing issue_type
1086 |             },
1087 |             {
1088 |                 "project_key": "TEST",
1089 |                 "summary": "Test Issue 2",
1090 |                 "issue_type": "Bug",
1091 |             },
1092 |         ]
1093 | 
1094 |         # Verify it raises ValueError
1095 |         with pytest.raises(ValueError) as exc_info:
1096 |             issues_mixin.batch_create_issues(issues)
1097 | 
1098 |         assert "Missing required fields" in str(exc_info.value)
1099 |         assert not issues_mixin.jira.create_issues.called
1100 | 
1101 |     def test_batch_create_issues_partial_failure(self, issues_mixin: IssuesMixin):
1102 |         """Test batch_create_issues when some issues fail to create."""
1103 |         # Setup test data
1104 |         issues = [
1105 |             {
1106 |                 "project_key": "TEST",
1107 |                 "summary": "Test Issue 1",
1108 |                 "issue_type": "Task",
1109 |             },
1110 |             {
1111 |                 "project_key": "TEST",
1112 |                 "summary": "Test Issue 2",
1113 |                 "issue_type": "Bug",
1114 |             },
1115 |         ]
1116 | 
1117 |         # Mock bulk create response with an error
1118 |         bulk_response = {
1119 |             "issues": [
1120 |                 {"id": "1", "key": "TEST-1", "self": "http://example.com/TEST-1"},
1121 |             ],
1122 |             "errors": [{"issue": {"key": None}, "error": "Invalid issue type"}],
1123 |         }
1124 |         issues_mixin.jira.create_issues.return_value = bulk_response
1125 | 
1126 |         # Mock get_issue response for successful creation
1127 |         issues_mixin.jira.get_issue.return_value = {
1128 |             "id": "1",
1129 |             "key": "TEST-1",
1130 |             "fields": {"summary": "Test Issue 1"},
1131 |         }
1132 | 
1133 |         # Call the method
1134 |         result = issues_mixin.batch_create_issues(issues)
1135 | 
1136 |         # Verify results - should have only the first issue
1137 |         assert len(result) == 1
1138 |         assert result[0].key == "TEST-1"
1139 | 
1140 |         # Verify error was logged
1141 |         issues_mixin.jira.create_issues.assert_called_once()
1142 |         assert len(issues_mixin.jira.get_issue.mock_calls) == 1
1143 | 
1144 |     def test_batch_create_issues_empty_list(self, issues_mixin: IssuesMixin):
1145 |         """Test batch_create_issues with an empty list."""
1146 |         result = issues_mixin.batch_create_issues([])
1147 |         assert result == []
1148 |         assert not issues_mixin.jira.create_issues.called
1149 | 
1150 |     def test_batch_create_issues_with_components(self, issues_mixin: IssuesMixin):
1151 |         """Test batch_create_issues with component handling."""
1152 |         # Setup test data with various component formats
1153 |         issues = [
1154 |             {
1155 |                 "project_key": "TEST",
1156 |                 "summary": "Test Issue 1",
1157 |                 "issue_type": "Task",
1158 |                 "components": ["Frontend", "", None, "  Backend  "],
1159 |             }
1160 |         ]
1161 | 
1162 |         # Mock responses
1163 |         bulk_response = {
1164 |             "issues": [
1165 |                 {"id": "1", "key": "TEST-1", "self": "http://example.com/TEST-1"},
1166 |             ],
1167 |             "errors": [],
1168 |         }
1169 |         issues_mixin.jira.create_issues.return_value = bulk_response
1170 |         issues_mixin.jira.get_issue.return_value = {
1171 |             "id": "1",
1172 |             "key": "TEST-1",
1173 |             "fields": {"summary": "Test Issue 1"},
1174 |         }
1175 | 
1176 |         # Call the method
1177 |         result = issues_mixin.batch_create_issues(issues)
1178 | 
1179 |         # Verify results
1180 |         assert len(result) == 1
1181 | 
1182 |         # Verify components were properly formatted
1183 |         call_args = issues_mixin.jira.create_issues.call_args[0][0]
1184 |         assert len(call_args) == 1
1185 |         components = call_args[0]["fields"]["components"]
1186 |         assert len(components) == 2
1187 |         assert components[0]["name"] == "Frontend"
1188 |         assert components[1]["name"] == "Backend"
1189 | 
1190 |     def test_add_assignee_to_fields_cloud(self, issues_mixin: IssuesMixin):
1191 |         """Test _add_assignee_to_fields for Cloud instance."""
1192 |         # Set up cloud config
1193 |         issues_mixin.config = MagicMock()
1194 |         issues_mixin.config.is_cloud = True
1195 | 
1196 |         # Test fields dict
1197 |         fields = {}
1198 | 
1199 |         # Call the method
1200 |         issues_mixin._add_assignee_to_fields(fields, "account-123")
1201 | 
1202 |         # Verify result
1203 |         assert fields["assignee"] == {"accountId": "account-123"}
1204 | 
1205 |     def test_add_assignee_to_fields_server_dc(self, issues_mixin: IssuesMixin):
1206 |         """Test _add_assignee_to_fields for Server/Data Center instance."""
1207 |         # Set up Server/DC config
1208 |         issues_mixin.config = MagicMock()
1209 |         issues_mixin.config.is_cloud = False
1210 | 
1211 |         # Test fields dict
1212 |         fields = {}
1213 | 
1214 |         # Call the method
1215 |         issues_mixin._add_assignee_to_fields(fields, "jdoe")
1216 | 
1217 |         # Verify result
1218 |         assert fields["assignee"] == {"name": "jdoe"}
1219 | 
1220 |     def test_batch_get_changelogs_not_cloud(self, issues_mixin: IssuesMixin):
1221 |         """Test batch_get_changelogs method on non-cloud instance."""
1222 |         issues_mixin.config = MagicMock()
1223 |         issues_mixin.config.is_cloud = False
1224 | 
1225 |         with pytest.raises(NotImplementedError):
1226 |             issues_mixin.batch_get_changelogs(
1227 |                 issue_ids_or_keys=["TEST-123"],
1228 |                 fields=["summary", "description"],
1229 |             )
1230 | 
1231 |     def test_batch_get_changelogs_cloud(self, issues_mixin: IssuesMixin):
1232 |         """Test batch_get_changelogs method on cloud instance."""
1233 |         issues_mixin.config = MagicMock()
1234 |         issues_mixin.config.is_cloud = True
1235 | 
1236 |         # Mock get_paged result
1237 |         mock_get_paged_result = [
1238 |             {
1239 |                 "issueChangeLogs": [
1240 |                     {
1241 |                         "issueId": "TEST-1",
1242 |                         "changeHistories": [
1243 |                             {
1244 |                                 "id": "10001",
1245 |                                 "author": {
1246 |                                     "accountId": "user123",
1247 |                                     "displayName": "Test User 1",
1248 |                                     "active": True,
1249 |                                     "timeZone": "UTC",
1250 |                                     "accountType": "atlassian",
1251 |                                 },
1252 |                                 "created": "2024-01-05T10:06:03.548+0800",
1253 |                                 "items": [
1254 |                                     {
1255 |                                         "field": "IssueParentAssociation",
1256 |                                         "fieldtype": "jira",
1257 |                                         "from": None,
1258 |                                         "fromString": None,
1259 |                                         "to": "1001",
1260 |                                         "toString": "TEST-100",
1261 |                                     }
1262 |                                 ],
1263 |                             }
1264 |                         ],
1265 |                     },
1266 |                     {
1267 |                         "issueId": "TEST-2",
1268 |                         "changeHistories": [
1269 |                             {
1270 |                                 "id": "10002",
1271 |                                 "author": {
1272 |                                     "accountId": "user456",
1273 |                                     "displayName": "Test User 2",
1274 |                                     "active": True,
1275 |                                     "timeZone": "UTC",
1276 |                                     "accountType": "atlassian",
1277 |                                 },
1278 |                                 "created": "1704106800000",  # 2024-01-01
1279 |                                 "items": [
1280 |                                     {
1281 |                                         "field": "Parent",
1282 |                                         "fieldtype": "jira",
1283 |                                         "from": None,
1284 |                                         "fromString": None,
1285 |                                         "to": "1002",
1286 |                                         "toString": "TEST-200",
1287 |                                     }
1288 |                                 ],
1289 |                             },
1290 |                             {
1291 |                                 "id": "10003",
1292 |                                 "author": {
1293 |                                     "accountId": "user789",
1294 |                                     "displayName": "Test User 3",
1295 |                                     "active": True,
1296 |                                     "timeZone": "UTC",
1297 |                                     "accountType": "atlassian",
1298 |                                 },
1299 |                                 "created": "2024-01-06T10:06:03.548+0800",
1300 |                                 "items": [
1301 |                                     {
1302 |                                         "field": "Parent",
1303 |                                         "fieldtype": "jira",
1304 |                                         "from": "1002",
1305 |                                         "fromString": "TEST-200",
1306 |                                         "to": "1003",
1307 |                                         "toString": "TEST-300",
1308 |                                     }
1309 |                                 ],
1310 |                             },
1311 |                         ],
1312 |                     },
1313 |                 ],
1314 |                 "nextPageToken": "token1",
1315 |             },
1316 |             {
1317 |                 "issueChangeLogs": [
1318 |                     {
1319 |                         "issueId": "TEST-2",
1320 |                         "changeHistories": [
1321 |                             {
1322 |                                 "id": "10004",
1323 |                                 "author": {
1324 |                                     "accountId": "user123",
1325 |                                     "displayName": "Test User 1",
1326 |                                     "active": True,
1327 |                                     "timeZone": "UTC",
1328 |                                     "accountType": "atlassian",
1329 |                                 },
1330 |                                 "created": "2024-01-10T10:06:03.548+0800",
1331 |                                 "items": [
1332 |                                     {
1333 |                                         "field": "Parent",
1334 |                                         "fieldtype": "jira",
1335 |                                         "from": "1003",
1336 |                                         "fromString": "TEST-300",
1337 |                                         "to": "1004",
1338 |                                         "toString": "TEST-400",
1339 |                                     }
1340 |                                 ],
1341 |                             }
1342 |                         ],
1343 |                     }
1344 |                 ],
1345 |             },
1346 |         ]
1347 | 
1348 |         # Expected result
1349 |         expected_result = [
1350 |             {
1351 |                 "assignee": {"display_name": "Unassigned"},
1352 |                 "changelogs": [
1353 |                     {
1354 |                         "author": {
1355 |                             "avatar_url": None,
1356 |                             "display_name": "Test User 1",
1357 |                             "email": None,
1358 |                             "name": "Test User 1",
1359 |                         },
1360 |                         "created": "2024-01-05 10:06:03.548000+08:00",
1361 |                         "items": [
1362 |                             {
1363 |                                 "field": "IssueParentAssociation",
1364 |                                 "fieldtype": "jira",
1365 |                                 "to_id": "1001",
1366 |                                 "to_string": "TEST-100",
1367 |                             },
1368 |                         ],
1369 |                     },
1370 |                 ],
1371 |                 "id": "TEST-1",
1372 |                 "key": "UNKNOWN-0",
1373 |                 "summary": "",
1374 |             },
1375 |             {
1376 |                 "assignee": {"display_name": "Unassigned"},
1377 |                 "changelogs": [
1378 |                     {
1379 |                         "author": {
1380 |                             "avatar_url": None,
1381 |                             "display_name": "Test User 2",
1382 |                             "email": None,
1383 |                             "name": "Test User 2",
1384 |                         },
1385 |                         "created": "2024-01-01 11:00:00+00:00",
1386 |                         "items": [
1387 |                             {
1388 |                                 "field": "Parent",
1389 |                                 "fieldtype": "jira",
1390 |                                 "to_id": "1002",
1391 |                                 "to_string": "TEST-200",
1392 |                             },
1393 |                         ],
1394 |                     },
1395 |                     {
1396 |                         "author": {
1397 |                             "avatar_url": None,
1398 |                             "display_name": "Test User 3",
1399 |                             "email": None,
1400 |                             "name": "Test User 3",
1401 |                         },
1402 |                         "created": "2024-01-06 10:06:03.548000+08:00",
1403 |                         "items": [
1404 |                             {
1405 |                                 "field": "Parent",
1406 |                                 "fieldtype": "jira",
1407 |                                 "from_id": "1002",
1408 |                                 "from_string": "TEST-200",
1409 |                                 "to_id": "1003",
1410 |                                 "to_string": "TEST-300",
1411 |                             },
1412 |                         ],
1413 |                     },
1414 |                     {
1415 |                         "author": {
1416 |                             "avatar_url": None,
1417 |                             "display_name": "Test User 1",
1418 |                             "email": None,
1419 |                             "name": "Test User 1",
1420 |                         },
1421 |                         "created": "2024-01-10 10:06:03.548000+08:00",
1422 |                         "items": [
1423 |                             {
1424 |                                 "field": "Parent",
1425 |                                 "fieldtype": "jira",
1426 |                                 "from_id": "1003",
1427 |                                 "from_string": "TEST-300",
1428 |                                 "to_id": "1004",
1429 |                                 "to_string": "TEST-400",
1430 |                             },
1431 |                         ],
1432 |                     },
1433 |                 ],
1434 |                 "id": "TEST-2",
1435 |                 "key": "UNKNOWN-0",
1436 |                 "summary": "",
1437 |             },
1438 |         ]
1439 | 
1440 |         # Mock the get_paged method
1441 |         issues_mixin.get_paged = MagicMock(return_value=mock_get_paged_result)
1442 | 
1443 |         # Call the method
1444 |         result = issues_mixin.batch_get_changelogs(
1445 |             issue_ids_or_keys=["TEST-1", "TEST-2"],
1446 |             fields=["Parent"],
1447 |         )
1448 | 
1449 |         # Verify the result
1450 |         simplified_result = [issue.to_simplified_dict() for issue in result]
1451 |         assert simplified_result == expected_result
1452 | 
1453 |         # Verify the method was called with the correct arguments
1454 |         issues_mixin.get_paged.assert_called_once_with(
1455 |             method="post",
1456 |             url=issues_mixin.jira.resource_url("changelog/bulkfetch"),
1457 |             params_or_json={
1458 |                 "fieldIds": ["Parent"],
1459 |                 "issueIdsOrKeys": ["TEST-1", "TEST-2"],
1460 |             },
1461 |         )
1462 | 
1463 |     def test_create_issue_with_labels(self, issues_mixin: IssuesMixin):
1464 |         """Test creating an issue with labels in additional_fields."""
1465 |         # Mock create_issue response
1466 |         create_response = {"id": "12345", "key": "TEST-123"}
1467 |         issues_mixin.jira.create_issue.return_value = create_response
1468 | 
1469 |         # Mock the issue data for get_issue
1470 |         issue_data = {
1471 |             "id": "12345",
1472 |             "key": "TEST-123",
1473 |             "fields": {
1474 |                 "summary": "Test Issue",
1475 |                 "description": "This is a test issue",
1476 |                 "status": {"name": "Open"},
1477 |                 "issuetype": {"name": "Bug"},
1478 |                 "labels": ["bug", "frontend"],
1479 |             },
1480 |         }
1481 |         issues_mixin.jira.get_issue.return_value = issue_data
1482 | 
1483 |         # Create the issue with labels as a list
1484 |         result = issues_mixin.create_issue(
1485 |             project_key="TEST",
1486 |             summary="Test Issue",
1487 |             issue_type="Bug",
1488 |             description="This is a test issue",
1489 |             labels=["bug", "frontend"],
1490 |         )
1491 | 
1492 |         # Verify the API call
1493 |         issues_mixin.jira.create_issue.assert_called_once()
1494 |         call_kwargs = issues_mixin.jira.create_issue.call_args[1]
1495 |         assert "fields" in call_kwargs
1496 |         fields = call_kwargs["fields"]
1497 | 
1498 |         # Verify labels were added to the fields
1499 |         assert "labels" in fields
1500 |         assert fields["labels"] == ["bug", "frontend"]
1501 | 
1502 |         # Verify result
1503 |         assert result.key == "TEST-123"
1504 |         assert result.labels == ["bug", "frontend"]
1505 | 
1506 |     def test_create_issue_with_labels_as_string(self, issues_mixin: IssuesMixin):
1507 |         """Test creating an issue with labels as comma-separated string in additional_fields."""
1508 |         # Mock create_issue response
1509 |         create_response = {"id": "12345", "key": "TEST-123"}
1510 |         issues_mixin.jira.create_issue.return_value = create_response
1511 | 
1512 |         # Mock the issue data for get_issue
1513 |         issue_data = {
1514 |             "id": "12345",
1515 |             "key": "TEST-123",
1516 |             "fields": {
1517 |                 "summary": "Test Issue",
1518 |                 "description": "This is a test issue",
1519 |                 "status": {"name": "Open"},
1520 |                 "issuetype": {"name": "Bug"},
1521 |                 "labels": ["bug", "frontend"],
1522 |             },
1523 |         }
1524 |         issues_mixin.jira.get_issue.return_value = issue_data
1525 | 
1526 |         # Create the issue with labels as a comma-separated string
1527 |         # Pass labels directly instead of through additional_fields
1528 |         result = issues_mixin.create_issue(
1529 |             project_key="TEST",
1530 |             summary="Test Issue",
1531 |             issue_type="Bug",
1532 |             description="This is a test issue",
1533 |             labels="bug,frontend",  # Pass as string and let _format_field_value_for_write handle it
1534 |         )
1535 | 
1536 |         # Verify the API call
1537 |         issues_mixin.jira.create_issue.assert_called_once()
1538 |         call_kwargs = issues_mixin.jira.create_issue.call_args[1]
1539 |         assert "fields" in call_kwargs
1540 |         fields = call_kwargs["fields"]
1541 | 
1542 |         # Verify labels were parsed and added to the fields
1543 |         assert "labels" in fields
1544 |         assert fields["labels"] == ["bug", "frontend"]
1545 | 
1546 |         # Verify result
1547 |         assert result.key == "TEST-123"
1548 |         assert result.labels == ["bug", "frontend"]
1549 | 
1550 |     def test_get_issue_with_config_projects_filter_restricted(
1551 |         self, issues_mixin: IssuesMixin
1552 |     ):
1553 |         """Test get_issue with projects filter from config - restricted case."""
1554 |         # Setup mock response
1555 |         mock_issues = {
1556 |             "issues": [
1557 |                 {
1558 |                     "id": "10001",
1559 |                     "key": "TEST-123",
1560 |                     "fields": {
1561 |                         "summary": "Test issue",
1562 |                         "issuetype": {"name": "Bug"},
1563 |                         "status": {"name": "Open"},
1564 |                     },
1565 |                 }
1566 |             ],
1567 |             "total": 1,
1568 |             "startAt": 0,
1569 |             "maxResults": 50,
1570 |         }
1571 |         issues_mixin.jira.jql.return_value = mock_issues
1572 |         issues_mixin.config.url = "https://example.atlassian.net"
1573 |         issues_mixin.config.projects_filter = "DEV"
1574 | 
1575 |         # Mock the API to raise an exception
1576 |         issues_mixin.jira.get_issue.side_effect = Exception("API error")
1577 | 
1578 |         # Call the method and verify it raises the expected exception
1579 |         with pytest.raises(
1580 |             Exception,
1581 |             match=(
1582 |                 "Error retrieving issue TEST-123: "
1583 |                 "Issue with project prefix 'TEST' are restricted by configuration"
1584 |             ),
1585 |         ):
1586 |             issues_mixin.get_issue("TEST-123")
1587 | 
1588 |     def test_get_issue_with_config_projects_filter_allowed(
1589 |         self, issues_mixin: IssuesMixin
1590 |     ):
1591 |         """Test get_issue with projects filter from config - allowed case."""
1592 |         # Setup mock response for a project that matches the filter
1593 |         mock_issue_data = {
1594 |             "id": "10001",
1595 |             "key": "DEV-123",
1596 |             "fields": {
1597 |                 "summary": "Test issue",
1598 |                 "description": "This is a test issue",
1599 |                 "status": {"name": "Open"},
1600 |                 "issuetype": {"name": "Bug"},
1601 |             },
1602 |         }
1603 |         issues_mixin.jira.get_issue.return_value = mock_issue_data
1604 |         issues_mixin.config.url = "https://example.atlassian.net"
1605 |         issues_mixin.config.projects_filter = "DEV"
1606 | 
1607 |         # Call the method
1608 |         result = issues_mixin.get_issue("DEV-123")
1609 | 
1610 |         # Verify the API call was made correctly
1611 |         issues_mixin.jira.get_issue.assert_called_once_with(
1612 |             "DEV-123",
1613 |             expand=None,
1614 |             fields=ANY,
1615 |             properties=None,
1616 |             update_history=True,
1617 |         )
1618 | 
1619 |         # Verify the result
1620 |         assert isinstance(result, JiraIssue)
1621 |         assert result.key == "DEV-123"
1622 |         assert result.summary == "Test issue"
1623 | 
1624 |     def test_get_issue_with_multiple_projects_filter(self, issues_mixin: IssuesMixin):
1625 |         """Test get_issue with multiple projects in the filter."""
1626 |         # Setup mock response for a project that matches one of the multiple filters
1627 |         mock_issue_data = {
1628 |             "id": "10001",
1629 |             "key": "PROD-123",
1630 |             "fields": {
1631 |                 "summary": "Production issue",
1632 |                 "description": "This is a production issue",
1633 |                 "status": {"name": "Open"},
1634 |                 "issuetype": {"name": "Bug"},
1635 |             },
1636 |         }
1637 |         issues_mixin.jira.get_issue.return_value = mock_issue_data
1638 |         issues_mixin.config.url = "https://example.atlassian.net"
1639 |         issues_mixin.config.projects_filter = "DEV,PROD"
1640 | 
1641 |         # Call the method
1642 |         result = issues_mixin.get_issue("PROD-123")
1643 | 
1644 |         # Verify the API call was made correctly
1645 |         issues_mixin.jira.get_issue.assert_called_once_with(
1646 |             "PROD-123",
1647 |             expand=None,
1648 |             fields=ANY,
1649 |             properties=None,
1650 |             update_history=True,
1651 |         )
1652 | 
1653 |         # Verify the result
1654 |         assert isinstance(result, JiraIssue)
1655 |         assert result.key == "PROD-123"
1656 |         assert result.summary == "Production issue"
1657 | 
1658 |     def test_get_issue_with_whitespace_in_projects_filter(
1659 |         self, issues_mixin: IssuesMixin
1660 |     ):
1661 |         """Test get_issue with extra whitespace in the projects filter."""
1662 |         # Setup mock response for a project that matches the filter with whitespace
1663 |         mock_issue_data = {
1664 |             "id": "10001",
1665 |             "key": "DEV-123",
1666 |             "fields": {
1667 |                 "summary": "Development issue",
1668 |                 "description": "This is a development issue",
1669 |                 "status": {"name": "Open"},
1670 |                 "issuetype": {"name": "Bug"},
1671 |             },
1672 |         }
1673 |         issues_mixin.jira.get_issue.return_value = mock_issue_data
1674 |         issues_mixin.config.url = "https://example.atlassian.net"
1675 |         issues_mixin.config.projects_filter = " DEV , PROD "  # Extra whitespace
1676 | 
1677 |         # Call the method
1678 |         result = issues_mixin.get_issue("DEV-123")
1679 | 
1680 |         # Verify the API call was made correctly
1681 |         issues_mixin.jira.get_issue.assert_called_once_with(
1682 |             "DEV-123",
1683 |             expand=None,
1684 |             fields=ANY,
1685 |             properties=None,
1686 |             update_history=True,
1687 |         )
1688 | 
1689 |         # Verify the result
1690 |         assert isinstance(result, JiraIssue)
1691 |         assert result.key == "DEV-123"
1692 |         assert result.summary == "Development issue"
1693 | 
```

--------------------------------------------------------------------------------
/tests/unit/models/test_jira_models.py:
--------------------------------------------------------------------------------

```python
   1 | """
   2 | Tests for the Jira Pydantic models.
   3 | 
   4 | These tests validate the conversion of Jira API responses to structured models
   5 | and the simplified dictionary conversion for API responses.
   6 | """
   7 | 
   8 | import os
   9 | import re
  10 | 
  11 | import pytest
  12 | 
  13 | from src.mcp_atlassian.models.constants import (
  14 |     EMPTY_STRING,
  15 |     JIRA_DEFAULT_ID,
  16 |     JIRA_DEFAULT_PROJECT,
  17 |     UNKNOWN,
  18 | )
  19 | from src.mcp_atlassian.models.jira import (
  20 |     JiraComment,
  21 |     JiraIssue,
  22 |     JiraIssueLink,
  23 |     JiraIssueLinkType,
  24 |     JiraIssueType,
  25 |     JiraLinkedIssue,
  26 |     JiraLinkedIssueFields,
  27 |     JiraPriority,
  28 |     JiraProject,
  29 |     JiraResolution,
  30 |     JiraSearchResult,
  31 |     JiraStatus,
  32 |     JiraStatusCategory,
  33 |     JiraTimetracking,
  34 |     JiraTransition,
  35 |     JiraUser,
  36 |     JiraWorklog,
  37 | )
  38 | 
  39 | # Optional: Import real API client for optional real-data testing
  40 | try:
  41 |     from atlassian import Jira
  42 | 
  43 |     from src.mcp_atlassian.jira import JiraConfig, JiraFetcher
  44 |     from src.mcp_atlassian.jira.issues import IssuesMixin
  45 |     from src.mcp_atlassian.jira.projects import ProjectsMixin
  46 |     from src.mcp_atlassian.jira.transitions import TransitionsMixin
  47 |     from src.mcp_atlassian.jira.worklog import WorklogMixin
  48 | 
  49 |     real_api_available = True
  50 | except ImportError:
  51 |     real_api_available = False
  52 | 
  53 |     # Create a module-level namespace for dummy classes
  54 |     class _DummyClasses:
  55 |         """Namespace for dummy classes when real imports fail."""
  56 | 
  57 |         class JiraFetcher:
  58 |             pass
  59 | 
  60 |         class JiraConfig:
  61 |             @staticmethod
  62 |             def from_env():
  63 |                 return None
  64 | 
  65 |         class IssuesMixin:
  66 |             pass
  67 | 
  68 |         class ProjectsMixin:
  69 |             pass
  70 | 
  71 |         class TransitionsMixin:
  72 |             pass
  73 | 
  74 |         class WorklogMixin:
  75 |             pass
  76 | 
  77 |         class Jira:
  78 |             pass
  79 | 
  80 |     # Assign dummy classes to module namespace
  81 |     JiraFetcher = _DummyClasses.JiraFetcher
  82 |     JiraConfig = _DummyClasses.JiraConfig
  83 |     IssuesMixin = _DummyClasses.IssuesMixin
  84 |     ProjectsMixin = _DummyClasses.ProjectsMixin
  85 |     TransitionsMixin = _DummyClasses.TransitionsMixin
  86 |     WorklogMixin = _DummyClasses.WorklogMixin
  87 |     Jira = _DummyClasses.Jira
  88 | 
  89 | 
  90 | class TestJiraUser:
  91 |     """Tests for the JiraUser model."""
  92 | 
  93 |     def test_from_api_response_with_valid_data(self):
  94 |         """Test creating a JiraUser from valid API data."""
  95 |         user_data = {
  96 |             "accountId": "user123",
  97 |             "displayName": "Test User",
  98 |             "emailAddress": "[email protected]",
  99 |             "active": True,
 100 |             "avatarUrls": {
 101 |                 "48x48": "https://example.com/avatar.png",
 102 |                 "24x24": "https://example.com/avatar-small.png",
 103 |             },
 104 |             "timeZone": "UTC",
 105 |         }
 106 |         user = JiraUser.from_api_response(user_data)
 107 |         assert user.account_id == "user123"
 108 |         assert user.display_name == "Test User"
 109 |         assert user.email == "[email protected]"
 110 |         assert user.active is True
 111 |         assert user.avatar_url == "https://example.com/avatar.png"
 112 |         assert user.time_zone == "UTC"
 113 | 
 114 |     def test_from_api_response_with_empty_data(self):
 115 |         """Test creating a JiraUser from empty data."""
 116 |         user = JiraUser.from_api_response({})
 117 |         assert user.account_id is None
 118 |         assert user.display_name == "Unassigned"
 119 |         assert user.email is None
 120 |         assert user.active is True
 121 |         assert user.avatar_url is None
 122 |         assert user.time_zone is None
 123 | 
 124 |     def test_from_api_response_with_none_data(self):
 125 |         """Test creating a JiraUser from None data."""
 126 |         user = JiraUser.from_api_response(None)
 127 |         assert user.account_id is None
 128 |         assert user.display_name == "Unassigned"
 129 |         assert user.email is None
 130 |         assert user.active is True
 131 |         assert user.avatar_url is None
 132 |         assert user.time_zone is None
 133 | 
 134 |     def test_to_simplified_dict(self):
 135 |         """Test converting JiraUser to a simplified dictionary."""
 136 |         user = JiraUser(
 137 |             account_id="user123",
 138 |             display_name="Test User",
 139 |             email="[email protected]",
 140 |             active=True,
 141 |             avatar_url="https://example.com/avatar.png",
 142 |             time_zone="UTC",
 143 |         )
 144 |         simplified = user.to_simplified_dict()
 145 |         assert isinstance(simplified, dict)
 146 |         assert simplified["display_name"] == "Test User"
 147 |         assert simplified["email"] == "[email protected]"
 148 |         assert simplified["avatar_url"] == "https://example.com/avatar.png"
 149 |         assert "account_id" not in simplified
 150 |         assert "time_zone" not in simplified
 151 | 
 152 | 
 153 | class TestJiraStatusCategory:
 154 |     """Tests for the JiraStatusCategory model."""
 155 | 
 156 |     def test_from_api_response_with_valid_data(self):
 157 |         """Test creating a JiraStatusCategory from valid API data."""
 158 |         data = {
 159 |             "id": 4,
 160 |             "key": "indeterminate",
 161 |             "name": "In Progress",
 162 |             "colorName": "yellow",
 163 |         }
 164 |         category = JiraStatusCategory.from_api_response(data)
 165 |         assert category.id == 4
 166 |         assert category.key == "indeterminate"
 167 |         assert category.name == "In Progress"
 168 |         assert category.color_name == "yellow"
 169 | 
 170 |     def test_from_api_response_with_empty_data(self):
 171 |         """Test creating a JiraStatusCategory from empty data."""
 172 |         category = JiraStatusCategory.from_api_response({})
 173 |         assert category.id == 0
 174 |         assert category.key == EMPTY_STRING
 175 |         assert category.name == UNKNOWN
 176 |         assert category.color_name == EMPTY_STRING
 177 | 
 178 | 
 179 | class TestJiraStatus:
 180 |     """Tests for the JiraStatus model."""
 181 | 
 182 |     def test_from_api_response_with_valid_data(self):
 183 |         """Test creating a JiraStatus from valid API data."""
 184 |         data = {
 185 |             "id": "10000",
 186 |             "name": "In Progress",
 187 |             "description": "Work is in progress",
 188 |             "iconUrl": "https://example.com/icon.png",
 189 |             "statusCategory": {
 190 |                 "id": 4,
 191 |                 "key": "indeterminate",
 192 |                 "name": "In Progress",
 193 |                 "colorName": "yellow",
 194 |             },
 195 |         }
 196 |         status = JiraStatus.from_api_response(data)
 197 |         assert status.id == "10000"
 198 |         assert status.name == "In Progress"
 199 |         assert status.description == "Work is in progress"
 200 |         assert status.icon_url == "https://example.com/icon.png"
 201 |         assert status.category is not None
 202 |         assert status.category.id == 4
 203 |         assert status.category.name == "In Progress"
 204 |         assert status.category.color_name == "yellow"
 205 | 
 206 |     def test_from_api_response_with_empty_data(self):
 207 |         """Test creating a JiraStatus from empty data."""
 208 |         status = JiraStatus.from_api_response({})
 209 |         assert status.id == JIRA_DEFAULT_ID
 210 |         assert status.name == UNKNOWN
 211 |         assert status.description is None
 212 |         assert status.icon_url is None
 213 |         assert status.category is None
 214 | 
 215 |     def test_to_simplified_dict(self):
 216 |         """Test converting JiraStatus to a simplified dictionary."""
 217 |         status = JiraStatus(
 218 |             id="10000",
 219 |             name="In Progress",
 220 |             description="Work is in progress",
 221 |             icon_url="https://example.com/icon.png",
 222 |             category=JiraStatusCategory(
 223 |                 id=4, key="indeterminate", name="In Progress", color_name="yellow"
 224 |             ),
 225 |         )
 226 |         simplified = status.to_simplified_dict()
 227 |         assert isinstance(simplified, dict)
 228 |         assert simplified["name"] == "In Progress"
 229 |         assert "category" in simplified
 230 |         assert simplified["category"] == "In Progress"
 231 |         assert "color" in simplified
 232 |         assert simplified["color"] == "yellow"
 233 |         assert "description" not in simplified
 234 | 
 235 | 
 236 | class TestJiraIssueType:
 237 |     """Tests for the JiraIssueType model."""
 238 | 
 239 |     def test_from_api_response_with_valid_data(self):
 240 |         """Test creating a JiraIssueType from valid API data."""
 241 |         data = {
 242 |             "id": "10000",
 243 |             "name": "Task",
 244 |             "description": "A task that needs to be done.",
 245 |             "iconUrl": "https://example.com/task-icon.png",
 246 |         }
 247 |         issue_type = JiraIssueType.from_api_response(data)
 248 |         assert issue_type.id == "10000"
 249 |         assert issue_type.name == "Task"
 250 |         assert issue_type.description == "A task that needs to be done."
 251 |         assert issue_type.icon_url == "https://example.com/task-icon.png"
 252 | 
 253 |     def test_from_api_response_with_empty_data(self):
 254 |         """Test creating a JiraIssueType from empty data."""
 255 |         issue_type = JiraIssueType.from_api_response({})
 256 |         assert issue_type.id == JIRA_DEFAULT_ID
 257 |         assert issue_type.name == UNKNOWN
 258 |         assert issue_type.description is None
 259 |         assert issue_type.icon_url is None
 260 | 
 261 |     def test_to_simplified_dict(self):
 262 |         """Test converting JiraIssueType to a simplified dictionary."""
 263 |         issue_type = JiraIssueType(
 264 |             id="10000",
 265 |             name="Task",
 266 |             description="A task that needs to be done.",
 267 |             icon_url="https://example.com/task-icon.png",
 268 |         )
 269 |         simplified = issue_type.to_simplified_dict()
 270 |         assert isinstance(simplified, dict)
 271 |         assert simplified["name"] == "Task"
 272 |         assert "id" not in simplified
 273 |         assert "description" not in simplified
 274 |         assert "icon_url" not in simplified
 275 | 
 276 | 
 277 | class TestJiraPriority:
 278 |     """Tests for the JiraPriority model."""
 279 | 
 280 |     def test_from_api_response_with_valid_data(self):
 281 |         """Test creating a JiraPriority from valid API data."""
 282 |         data = {
 283 |             "id": "3",
 284 |             "name": "Medium",
 285 |             "description": "Medium priority",
 286 |             "iconUrl": "https://example.com/medium-priority.png",
 287 |         }
 288 |         priority = JiraPriority.from_api_response(data)
 289 |         assert priority.id == "3"
 290 |         assert priority.name == "Medium"
 291 |         assert priority.description == "Medium priority"
 292 |         assert priority.icon_url == "https://example.com/medium-priority.png"
 293 | 
 294 |     def test_from_api_response_with_empty_data(self):
 295 |         """Test creating a JiraPriority from empty data."""
 296 |         priority = JiraPriority.from_api_response({})
 297 |         assert priority.id == JIRA_DEFAULT_ID
 298 |         assert priority.name == "None"  # Default for priority is 'None'
 299 |         assert priority.description is None
 300 |         assert priority.icon_url is None
 301 | 
 302 |     def test_to_simplified_dict(self):
 303 |         """Test converting JiraPriority to a simplified dictionary."""
 304 |         priority = JiraPriority(
 305 |             id="3",
 306 |             name="Medium",
 307 |             description="Medium priority",
 308 |             icon_url="https://example.com/medium-priority.png",
 309 |         )
 310 |         simplified = priority.to_simplified_dict()
 311 |         assert isinstance(simplified, dict)
 312 |         assert simplified["name"] == "Medium"
 313 |         assert "id" not in simplified
 314 |         assert "description" not in simplified
 315 |         assert "icon_url" not in simplified
 316 | 
 317 | 
 318 | class TestJiraComment:
 319 |     """Tests for the JiraComment model."""
 320 | 
 321 |     def test_from_api_response_with_valid_data(self):
 322 |         """Test creating a JiraComment from valid API data."""
 323 |         data = {
 324 |             "id": "10000",
 325 |             "body": "This is a test comment",
 326 |             "created": "2024-01-01T12:00:00.000+0000",
 327 |             "updated": "2024-01-01T12:00:00.000+0000",
 328 |             "author": {
 329 |                 "accountId": "user123",
 330 |                 "displayName": "Comment User",
 331 |                 "active": True,
 332 |             },
 333 |         }
 334 |         comment = JiraComment.from_api_response(data)
 335 |         assert comment.id == "10000"
 336 |         assert comment.body == "This is a test comment"
 337 |         assert comment.created == "2024-01-01T12:00:00.000+0000"
 338 |         assert comment.updated == "2024-01-01T12:00:00.000+0000"
 339 |         assert comment.author is not None
 340 |         assert comment.author.display_name == "Comment User"
 341 | 
 342 |     def test_from_api_response_with_empty_data(self):
 343 |         """Test creating a JiraComment from empty data."""
 344 |         comment = JiraComment.from_api_response({})
 345 |         assert comment.id == JIRA_DEFAULT_ID
 346 |         assert comment.body == EMPTY_STRING
 347 |         assert comment.created == EMPTY_STRING
 348 |         assert comment.updated == EMPTY_STRING
 349 |         assert comment.author is None
 350 | 
 351 |     def test_to_simplified_dict(self):
 352 |         """Test converting JiraComment to a simplified dictionary."""
 353 |         comment = JiraComment(
 354 |             id="10000",
 355 |             body="This is a test comment",
 356 |             created="2024-01-01T12:00:00.000+0000",
 357 |             updated="2024-01-01T12:00:00.000+0000",
 358 |             author=JiraUser(account_id="user123", display_name="Comment User"),
 359 |         )
 360 |         simplified = comment.to_simplified_dict()
 361 |         assert isinstance(simplified, dict)
 362 |         assert "body" in simplified
 363 |         assert simplified["body"] == "This is a test comment"
 364 |         assert "created" in simplified
 365 |         assert isinstance(simplified["created"], str)
 366 |         assert "author" in simplified
 367 |         assert isinstance(simplified["author"], dict)
 368 |         assert simplified["author"]["display_name"] == "Comment User"
 369 | 
 370 | 
 371 | class TestJiraTimetracking:
 372 |     """Tests for the JiraTimetracking model."""
 373 | 
 374 |     def test_from_api_response_with_valid_data(self):
 375 |         """Test creating a JiraTimetracking from valid API data."""
 376 |         data = {
 377 |             "originalEstimate": "2h",
 378 |             "remainingEstimate": "1h 30m",
 379 |             "timeSpent": "30m",
 380 |             "originalEstimateSeconds": 7200,
 381 |             "remainingEstimateSeconds": 5400,
 382 |             "timeSpentSeconds": 1800,
 383 |         }
 384 |         timetracking = JiraTimetracking.from_api_response(data)
 385 |         assert timetracking.original_estimate == "2h"
 386 |         assert timetracking.remaining_estimate == "1h 30m"
 387 |         assert timetracking.time_spent == "30m"
 388 |         assert timetracking.original_estimate_seconds == 7200
 389 |         assert timetracking.remaining_estimate_seconds == 5400
 390 |         assert timetracking.time_spent_seconds == 1800
 391 | 
 392 |     def test_from_api_response_with_empty_data(self):
 393 |         """Test creating a JiraTimetracking from empty data."""
 394 |         timetracking = JiraTimetracking.from_api_response({})
 395 |         assert timetracking.original_estimate is None
 396 |         assert timetracking.remaining_estimate is None
 397 |         assert timetracking.time_spent is None
 398 |         assert timetracking.original_estimate_seconds is None
 399 |         assert timetracking.remaining_estimate_seconds is None
 400 |         assert timetracking.time_spent_seconds is None
 401 | 
 402 |     def test_from_api_response_with_none_data(self):
 403 |         """Test creating a JiraTimetracking from None data."""
 404 |         timetracking = JiraTimetracking.from_api_response(None)
 405 |         assert timetracking is not None
 406 |         assert timetracking.original_estimate is None
 407 |         assert timetracking.remaining_estimate is None
 408 |         assert timetracking.time_spent is None
 409 |         assert timetracking.original_estimate_seconds is None
 410 |         assert timetracking.remaining_estimate_seconds is None
 411 |         assert timetracking.time_spent_seconds is None
 412 | 
 413 |     def test_to_simplified_dict(self):
 414 |         """Test converting JiraTimetracking to a simplified dictionary."""
 415 |         timetracking = JiraTimetracking(
 416 |             original_estimate="2h",
 417 |             remaining_estimate="1h 30m",
 418 |             time_spent="30m",
 419 |             original_estimate_seconds=7200,
 420 |             remaining_estimate_seconds=5400,
 421 |             time_spent_seconds=1800,
 422 |         )
 423 |         simplified = timetracking.to_simplified_dict()
 424 |         assert isinstance(simplified, dict)
 425 |         assert simplified["original_estimate"] == "2h"
 426 |         assert simplified["remaining_estimate"] == "1h 30m"
 427 |         assert simplified["time_spent"] == "30m"
 428 |         assert "original_estimate_seconds" not in simplified
 429 |         assert "remaining_estimate_seconds" not in simplified
 430 |         assert "time_spent_seconds" not in simplified
 431 | 
 432 | 
 433 | class TestJiraIssue:
 434 |     """Tests for the JiraIssue model."""
 435 | 
 436 |     def test_from_api_response_with_valid_data(self, jira_issue_data):
 437 |         """Test creating a JiraIssue from valid API data."""
 438 |         issue = JiraIssue.from_api_response(jira_issue_data)
 439 | 
 440 |         assert issue.id == "12345"
 441 |         assert issue.key == "PROJ-123"
 442 |         assert issue.summary == "Test Issue Summary"
 443 |         assert issue.description == "This is a test issue description"
 444 |         assert issue.created == "2024-01-01T10:00:00.000+0000"
 445 |         assert issue.updated == "2024-01-02T15:30:00.000+0000"
 446 | 
 447 |         assert issue.status is not None
 448 |         assert issue.status.name == "In Progress"
 449 |         assert issue.status.category is not None
 450 |         assert issue.status.category.name == "In Progress"
 451 | 
 452 |         assert issue.issue_type is not None
 453 |         assert issue.issue_type.name == "Task"
 454 | 
 455 |         assert issue.priority is not None
 456 |         assert issue.priority.name == "Medium"
 457 | 
 458 |         assert issue.assignee is not None
 459 |         assert issue.assignee.display_name == "Test User"
 460 | 
 461 |         assert issue.reporter is not None
 462 |         assert issue.reporter.display_name == "Reporter User"
 463 | 
 464 |         assert len(issue.labels) == 1
 465 |         assert issue.labels[0] == "test-label"
 466 | 
 467 |         assert len(issue.comments) == 1
 468 |         assert issue.comments[0].body == "This is a test comment"
 469 | 
 470 |         assert isinstance(issue.fix_versions, list)
 471 |         assert "v1.0" in issue.fix_versions
 472 | 
 473 |         assert isinstance(issue.attachments, list)
 474 |         assert len(issue.attachments) == 1
 475 |         assert issue.attachments[0].filename == "test_attachment.txt"
 476 | 
 477 |         assert isinstance(issue.timetracking, JiraTimetracking)
 478 |         assert issue.timetracking.original_estimate == "1d"
 479 | 
 480 |         assert issue.project is not None
 481 |         assert issue.project.key == "PROJ"
 482 |         assert issue.project.name == "Test Project"
 483 |         assert issue.resolution is not None
 484 |         assert issue.resolution.name == "Fixed"
 485 |         assert issue.duedate == "2024-12-31"
 486 |         assert issue.resolutiondate == "2024-01-15T11:00:00.000+0000"
 487 |         assert issue.parent is not None
 488 |         assert issue.parent["key"] == "PROJ-122"
 489 |         assert issue.subtasks is not None
 490 |         assert len(issue.subtasks) == 1
 491 |         assert issue.subtasks[0]["key"] == "PROJ-124"
 492 |         assert issue.security is not None
 493 |         assert issue.security["name"] == "Internal"
 494 |         assert issue.worklog is not None
 495 |         assert issue.worklog["total"] == 0
 496 |         assert issue.worklog["maxResults"] == 20
 497 | 
 498 |         # Verify custom_fields structure after from_api_response
 499 |         assert "customfield_10001" in issue.custom_fields
 500 |         assert issue.custom_fields["customfield_10001"] == {
 501 |             "value": "Custom Text Field Value",
 502 |             "name": "My Custom Text Field",
 503 |         }
 504 |         assert "customfield_10002" in issue.custom_fields
 505 |         assert issue.custom_fields["customfield_10002"] == {
 506 |             "value": {"value": "Custom Select Value"},  # Original value is a dict
 507 |             "name": "My Custom Select",
 508 |         }
 509 | 
 510 |     def test_from_api_response_with_new_fields(self):
 511 |         """Test creating a JiraIssue focusing on parsing the new fields."""
 512 |         # Construct local mock data including the new fields
 513 |         local_issue_data = {
 514 |             "id": "9999",
 515 |             "key": "NEW-1",
 516 |             "fields": {
 517 |                 "summary": "Issue testing new fields",
 518 |                 "project": {
 519 |                     "id": "10001",
 520 |                     "key": "NEWPROJ",
 521 |                     "name": "New Project",
 522 |                     "avatarUrls": {"48x48": "url"},
 523 |                 },
 524 |                 "resolution": {"id": "10002", "name": "Fixed"},
 525 |                 "duedate": "2025-01-31",
 526 |                 "resolutiondate": "2024-08-01T12:00:00.000+0000",
 527 |                 "parent": {
 528 |                     "id": "9998",
 529 |                     "key": "NEW-0",
 530 |                     "fields": {"summary": "Parent Task"},
 531 |                 },
 532 |                 "subtasks": [
 533 |                     {"id": "10000", "key": "NEW-2", "fields": {"summary": "Subtask 1"}},
 534 |                     {"id": "10001", "key": "NEW-3", "fields": {"summary": "Subtask 2"}},
 535 |                 ],
 536 |                 "security": {"id": "10003", "name": "Dev Only"},
 537 |                 "worklog": {"total": 2, "maxResults": 20, "worklogs": []},
 538 |             },
 539 |         }
 540 |         issue = JiraIssue.from_api_response(local_issue_data)
 541 | 
 542 |         assert issue.id == "9999"
 543 |         assert issue.key == "NEW-1"
 544 |         assert issue.summary == "Issue testing new fields"
 545 | 
 546 |         # Assertions for new fields using LOCAL data
 547 |         assert isinstance(issue.project, JiraProject)
 548 |         assert issue.project.key == "NEWPROJ"
 549 |         assert issue.project.name == "New Project"
 550 |         assert isinstance(issue.resolution, JiraResolution)
 551 |         assert issue.resolution.name == "Fixed"
 552 |         assert issue.duedate == "2025-01-31"
 553 |         assert issue.resolutiondate == "2024-08-01T12:00:00.000+0000"
 554 |         assert isinstance(issue.parent, dict)
 555 |         assert issue.parent["key"] == "NEW-0"
 556 |         assert isinstance(issue.subtasks, list)
 557 |         assert len(issue.subtasks) == 2
 558 |         assert issue.subtasks[0]["key"] == "NEW-2"
 559 |         assert isinstance(issue.security, dict)
 560 |         assert issue.security["name"] == "Dev Only"
 561 |         assert isinstance(issue.worklog, dict)
 562 |         assert issue.worklog["total"] == 2
 563 | 
 564 |     def test_from_api_response_with_issuelinks(self, jira_issue_data):
 565 |         """Test creating a JiraIssue with issue links."""
 566 |         # Augment jira_issue_data with mock issuelinks
 567 |         mock_issuelinks_data = [
 568 |             {
 569 |                 "id": "10000",
 570 |                 "type": {
 571 |                     "id": "10000",
 572 |                     "name": "Blocks",
 573 |                     "inward": "is blocked by",
 574 |                     "outward": "blocks",
 575 |                 },
 576 |                 "outwardIssue": {
 577 |                     "id": "10001",
 578 |                     "key": "PROJ-789",
 579 |                     "self": "https://example.atlassian.net/rest/api/2/issue/10001",
 580 |                     "fields": {
 581 |                         "summary": "Blocked Issue",
 582 |                         "status": {"name": "Open"},
 583 |                         "priority": {"name": "High"},
 584 |                         "issuetype": {"name": "Task"},
 585 |                     },
 586 |                 },
 587 |             },
 588 |             {
 589 |                 "id": "10001",
 590 |                 "type": {
 591 |                     "id": "10001",
 592 |                     "name": "Relates to",
 593 |                     "inward": "relates to",
 594 |                     "outward": "relates to",
 595 |                 },
 596 |                 "inwardIssue": {
 597 |                     "id": "10002",
 598 |                     "key": "PROJ-111",
 599 |                     "self": "https://example.atlassian.net/rest/api/2/issue/10002",
 600 |                     "fields": {
 601 |                         "summary": "Related Issue",
 602 |                         "status": {"name": "In Progress"},
 603 |                         "priority": {"name": "Medium"},
 604 |                         "issuetype": {"name": "Story"},
 605 |                     },
 606 |                 },
 607 |             },
 608 |         ]
 609 |         jira_issue_data_with_links = jira_issue_data.copy()
 610 |         # Ensure fields dictionary exists
 611 |         if "fields" not in jira_issue_data_with_links:
 612 |             jira_issue_data_with_links["fields"] = {}
 613 |         jira_issue_data_with_links["fields"]["issuelinks"] = mock_issuelinks_data
 614 | 
 615 |         issue = JiraIssue.from_api_response(
 616 |             jira_issue_data_with_links, requested_fields="*all"
 617 |         )
 618 | 
 619 |         assert issue.issuelinks is not None
 620 |         assert len(issue.issuelinks) == 2
 621 |         assert isinstance(issue.issuelinks[0], JiraIssueLink)
 622 | 
 623 |         # Check first link (outward)
 624 |         assert issue.issuelinks[0].id == "10000"
 625 |         assert issue.issuelinks[0].type is not None
 626 |         assert issue.issuelinks[0].type.name == "Blocks"
 627 |         assert issue.issuelinks[0].outward_issue is not None
 628 |         assert issue.issuelinks[0].outward_issue.key == "PROJ-789"
 629 |         assert issue.issuelinks[0].outward_issue.fields is not None
 630 |         assert issue.issuelinks[0].outward_issue.fields.summary == "Blocked Issue"
 631 |         assert issue.issuelinks[0].inward_issue is None
 632 | 
 633 |         # Test simplified dict output
 634 |         simplified = issue.to_simplified_dict()
 635 |         assert "issuelinks" in simplified
 636 |         assert len(simplified["issuelinks"]) == 2
 637 |         assert simplified["issuelinks"][0]["type"]["name"] == "Blocks"
 638 |         assert simplified["issuelinks"][0]["outward_issue"]["key"] == "PROJ-789"
 639 | 
 640 |     def test_from_api_response_with_empty_data(self):
 641 |         """Test creating a JiraIssue from empty data."""
 642 |         issue = JiraIssue.from_api_response({})
 643 |         assert issue.id == JIRA_DEFAULT_ID
 644 |         assert issue.key == "UNKNOWN-0"
 645 |         assert issue.summary == EMPTY_STRING
 646 |         assert issue.description is None
 647 |         assert issue.created == EMPTY_STRING
 648 |         assert issue.updated == EMPTY_STRING
 649 |         assert issue.status is None
 650 |         assert issue.issue_type is None
 651 |         assert issue.priority is None
 652 |         assert issue.assignee is None
 653 |         assert issue.reporter is None
 654 |         assert len(issue.labels) == 0
 655 |         assert len(issue.comments) == 0
 656 |         assert issue.project is None
 657 |         assert issue.resolution is None
 658 |         assert issue.duedate is None
 659 |         assert issue.resolutiondate is None
 660 |         assert issue.parent is None
 661 |         assert issue.subtasks == []
 662 |         assert issue.security is None
 663 |         assert issue.worklog is None
 664 | 
 665 |     def test_to_simplified_dict(self, jira_issue_data):
 666 |         """Test converting a JiraIssue to a simplified dictionary."""
 667 |         issue = JiraIssue.from_api_response(jira_issue_data)
 668 |         simplified = issue.to_simplified_dict()
 669 | 
 670 |         # Essential fields from original test
 671 |         assert isinstance(simplified, dict)
 672 |         assert "key" in simplified
 673 |         assert simplified["key"] == "PROJ-123"
 674 |         assert "summary" in simplified
 675 |         assert simplified["summary"] == "Test Issue Summary"
 676 | 
 677 |         assert "created" in simplified
 678 |         assert isinstance(simplified["created"], str)
 679 |         assert "updated" in simplified
 680 |         assert isinstance(simplified["updated"], str)
 681 | 
 682 |         if isinstance(simplified["status"], str):
 683 |             assert simplified["status"] == "In Progress"
 684 |         elif isinstance(simplified["status"], dict):
 685 |             assert simplified["status"]["name"] == "In Progress"
 686 | 
 687 |         if isinstance(simplified["issue_type"], str):
 688 |             assert simplified["issue_type"] == "Task"
 689 |         elif isinstance(simplified["issue_type"], dict):
 690 |             assert simplified["issue_type"]["name"] == "Task"
 691 | 
 692 |         if isinstance(simplified["priority"], str):
 693 |             assert simplified["priority"] == "Medium"
 694 |         elif isinstance(simplified["priority"], dict):
 695 |             assert simplified["priority"]["name"] == "Medium"
 696 | 
 697 |         assert "assignee" in simplified
 698 |         assert "reporter" in simplified
 699 | 
 700 |         # Test with "*all"
 701 |         issue_all = JiraIssue.from_api_response(
 702 |             jira_issue_data, requested_fields="*all"
 703 |         )
 704 |         simplified_all = issue_all.to_simplified_dict()
 705 | 
 706 |         # Check keys for all standard fields (new and old) are present
 707 |         all_standard_keys = {
 708 |             "id",
 709 |             "key",
 710 |             "summary",
 711 |             "description",
 712 |             "created",
 713 |             "updated",
 714 |             "status",
 715 |             "issue_type",
 716 |             "priority",
 717 |             "assignee",
 718 |             "reporter",
 719 |             "labels",
 720 |             "components",
 721 |             "timetracking",
 722 |             "comments",
 723 |             "attachments",
 724 |             "url",
 725 |             "epic_key",
 726 |             "epic_name",
 727 |             "fix_versions",
 728 |             "project",
 729 |             "resolution",
 730 |             "duedate",
 731 |             "resolutiondate",
 732 |             "parent",
 733 |             "subtasks",
 734 |             "security",
 735 |             "worklog",
 736 |             # Custom fields present in the mock data should be at the root level when requesting *all
 737 |             "customfield_10011",
 738 |             "customfield_10014",
 739 |             "customfield_10001",
 740 |             "customfield_10002",
 741 |             "customfield_10003",
 742 |         }
 743 |         assert all_standard_keys.issubset(simplified_all.keys())
 744 | 
 745 |         # Check values for new fields based on mock data
 746 |         assert simplified_all["project"]["key"] == "PROJ"
 747 |         assert simplified_all["resolution"]["name"] == "Fixed"
 748 |         assert simplified_all["duedate"] == "2024-12-31"
 749 |         assert simplified_all["resolutiondate"] == "2024-01-15T11:00:00.000+0000"
 750 |         assert simplified_all["parent"]["key"] == "PROJ-122"
 751 |         assert len(simplified_all["subtasks"]) == 1
 752 |         assert simplified_all["security"]["name"] == "Internal"
 753 |         assert isinstance(simplified_all["worklog"], dict)
 754 | 
 755 |         requested = [
 756 |             "key",
 757 |             "summary",
 758 |             "project",
 759 |             "resolution",
 760 |             "subtasks",
 761 |             "customfield_10011",
 762 |         ]
 763 |         issue_specific = JiraIssue.from_api_response(
 764 |             jira_issue_data, requested_fields=requested
 765 |         )
 766 |         simplified_specific = issue_specific.to_simplified_dict()
 767 | 
 768 |         # Check the requested keys are present
 769 |         assert set(simplified_specific.keys()) == {
 770 |             "id",
 771 |             "key",
 772 |             "summary",
 773 |             "project",
 774 |             "resolution",
 775 |             "subtasks",
 776 |             "customfield_10011",
 777 |         }
 778 | 
 779 |         # Check values based on mock data
 780 |         assert simplified_specific["project"]["key"] == "PROJ"
 781 |         assert simplified_specific["resolution"]["name"] == "Fixed"
 782 |         assert len(simplified_specific["subtasks"]) == 1
 783 |         # Check custom field output
 784 |         assert (
 785 |             simplified_specific["customfield_10011"]
 786 |             == {
 787 |                 "value": "Epic Name Example",
 788 |                 "name": "Epic Name",  # Comes from the "names" map in MOCK_JIRA_ISSUE_RESPONSE
 789 |             }
 790 |         )
 791 | 
 792 |     def test_find_custom_field_in_api_response(self):
 793 |         """Test the _find_custom_field_in_api_response method with different field patterns."""
 794 |         fields = {
 795 |             "customfield_10014": "EPIC-123",
 796 |             "customfield_10011": "Epic Name Test",
 797 |             "customfield_10000": "Another value",
 798 |             "schema": {
 799 |                 "fields": {
 800 |                     "customfield_10014": {"name": "Epic Link", "type": "string"},
 801 |                     "customfield_10011": {"name": "Epic Name", "type": "string"},
 802 |                     "customfield_10000": {"name": "Custom Field", "type": "string"},
 803 |                 }
 804 |             },
 805 |         }
 806 | 
 807 |         result = JiraIssue._find_custom_field_in_api_response(fields, ["Epic Link"])
 808 |         assert result == "EPIC-123"
 809 | 
 810 |         result = JiraIssue._find_custom_field_in_api_response(fields, ["Epic Name"])
 811 |         assert result == "Epic Name Test"
 812 | 
 813 |         result = JiraIssue._find_custom_field_in_api_response(fields, ["epic link"])
 814 |         assert result == "EPIC-123"
 815 | 
 816 |         result = JiraIssue._find_custom_field_in_api_response(
 817 |             fields, ["epic-link", "epiclink"]
 818 |         )
 819 |         assert result == "EPIC-123"
 820 | 
 821 |         result = JiraIssue._find_custom_field_in_api_response(
 822 |             fields, ["Non Existent Field"]
 823 |         )
 824 |         assert result is None
 825 | 
 826 |         result = JiraIssue._find_custom_field_in_api_response({}, ["Epic Link"])
 827 |         assert result is None
 828 | 
 829 |         result = JiraIssue._find_custom_field_in_api_response(None, ["Epic Link"])
 830 |         assert result is None
 831 | 
 832 |     def test_epic_field_extraction_different_field_ids(self):
 833 |         """Test finding epic fields with different customfield IDs."""
 834 |         test_data = {
 835 |             "id": "12345",
 836 |             "key": "PROJ-123",
 837 |             "fields": {
 838 |                 "summary": "Test Issue",
 839 |                 "customfield_20100": "EPIC-456",
 840 |                 "customfield_20200": "My Epic Name",
 841 |                 "schema": {
 842 |                     "fields": {
 843 |                         "customfield_20100": {"name": "Epic Link", "type": "string"},
 844 |                         "customfield_20200": {"name": "Epic Name", "type": "string"},
 845 |                     }
 846 |                 },
 847 |             },
 848 |         }
 849 |         issue = JiraIssue.from_api_response(test_data)
 850 |         assert issue.epic_key == "EPIC-456"
 851 |         assert issue.epic_name == "My Epic Name"
 852 | 
 853 |     def test_epic_field_extraction_fallback(self):
 854 |         """Test using common field names without relying on metadata."""
 855 |         test_data = {
 856 |             "id": "12345",
 857 |             "key": "PROJ-123",
 858 |             "fields": {
 859 |                 "summary": "Test Issue",
 860 |                 "customfield_10014": "EPIC-456",
 861 |                 "customfield_10011": "My Epic Name",
 862 |             },
 863 |         }
 864 | 
 865 |         original_method = JiraIssue._find_custom_field_in_api_response
 866 |         try:
 867 | 
 868 |             def mocked_find_field(fields, name_patterns):
 869 |                 normalized_patterns = []
 870 |                 for pattern in name_patterns:
 871 |                     norm_pattern = pattern.lower()
 872 |                     norm_pattern = re.sub(r"[_\-\s]", "", norm_pattern)
 873 |                     normalized_patterns.append(norm_pattern)
 874 | 
 875 |                 if any("epiclink" in p for p in normalized_patterns):
 876 |                     return fields.get("customfield_10014")
 877 |                 if any("epicname" in p for p in normalized_patterns):
 878 |                     return fields.get("customfield_10011")
 879 |                 return None
 880 | 
 881 |             JiraIssue._find_custom_field_in_api_response = staticmethod(
 882 |                 mocked_find_field
 883 |             )
 884 | 
 885 |             issue = JiraIssue.from_api_response(test_data)
 886 |             assert issue.epic_key == "EPIC-456"
 887 |             assert issue.epic_name == "My Epic Name"
 888 |         finally:
 889 |             JiraIssue._find_custom_field_in_api_response = staticmethod(original_method)
 890 | 
 891 |     def test_epic_field_extraction_advanced_patterns(self):
 892 |         """Test finding epic fields using various naming patterns."""
 893 |         test_data = {
 894 |             "id": "12345",
 895 |             "key": "PROJ-123",
 896 |             "fields": {
 897 |                 "summary": "Test Issue",
 898 |                 "customfield_12345": "EPIC-456",
 899 |                 "customfield_67890": "Epic Name Value",
 900 |                 "schema": {
 901 |                     "fields": {
 902 |                         "customfield_12345": {
 903 |                             "name": "Epic-Link Field",
 904 |                             "type": "string",
 905 |                         },
 906 |                         "customfield_67890": {"name": "EpicName", "type": "string"},
 907 |                     }
 908 |                 },
 909 |             },
 910 |         }
 911 |         issue = JiraIssue.from_api_response(test_data)
 912 |         assert issue.epic_key == "EPIC-456"
 913 |         assert issue.epic_name == "Epic Name Value"
 914 | 
 915 |     def test_fields_with_names(self):
 916 |         """Test using the names to find fields."""
 917 | 
 918 |         fields = {
 919 |             "customfield_55555": "EPIC-789",
 920 |             "customfield_66666": "Special Epic Name",
 921 |             "names": {
 922 |                 "customfield_55555": "Epic Link",
 923 |                 "customfield_66666": "Epic Name",
 924 |             },
 925 |         }
 926 | 
 927 |         result = JiraIssue._find_custom_field_in_api_response(fields, ["Epic Link"])
 928 |         assert result == "EPIC-789"
 929 | 
 930 |         test_data = {"id": "12345", "key": "PROJ-123", "fields": fields}
 931 |         issue = JiraIssue.from_api_response(test_data)
 932 |         assert issue.epic_key == "EPIC-789"
 933 |         assert issue.epic_name == "Special Epic Name"
 934 | 
 935 |     def test_jira_issue_with_custom_fields(self, jira_issue_data):
 936 |         """Test JiraIssue handling of custom fields."""
 937 |         issue = JiraIssue.from_api_response(jira_issue_data)
 938 |         simplified = issue.to_simplified_dict()
 939 |         assert simplified["key"] == "PROJ-123"
 940 |         assert simplified["summary"] == "Test Issue Summary"
 941 |         # By default (no requested_fields or default set), custom fields are not included
 942 |         # unless they are part of DEFAULT_READ_JIRA_FIELDS (which they are not).
 943 |         # So, this assertion should be that they are NOT present.
 944 |         assert "customfield_10001" not in simplified
 945 |         assert "customfield_10002" not in simplified
 946 |         assert "customfield_10003" not in simplified
 947 | 
 948 |         issue = JiraIssue.from_api_response(
 949 |             jira_issue_data, requested_fields="summary,customfield_10001"
 950 |         )
 951 |         simplified = issue.to_simplified_dict()
 952 |         assert "key" in simplified
 953 |         assert "summary" in simplified
 954 |         assert "customfield_10001" in simplified
 955 |         assert simplified["customfield_10001"]["value"] == "Custom Text Field Value"
 956 |         assert simplified["customfield_10001"]["name"] == "My Custom Text Field"
 957 |         assert "customfield_10002" not in simplified
 958 | 
 959 |         issue = JiraIssue.from_api_response(
 960 |             jira_issue_data, requested_fields=["key", "customfield_10002"]
 961 |         )
 962 |         simplified = issue.to_simplified_dict()
 963 |         assert "key" in simplified
 964 |         assert "customfield_10002" in simplified
 965 |         assert "summary" not in simplified
 966 |         assert "customfield_10001" not in simplified
 967 |         assert simplified["customfield_10002"]["value"] == "Custom Select Value"
 968 |         assert simplified["customfield_10002"]["name"] == "My Custom Select"
 969 | 
 970 |         issue = JiraIssue.from_api_response(jira_issue_data, requested_fields="*all")
 971 |         simplified = issue.to_simplified_dict()
 972 |         assert "key" in simplified
 973 |         assert "summary" in simplified
 974 |         assert "customfield_10001" in simplified
 975 |         assert simplified["customfield_10001"]["value"] == "Custom Text Field Value"
 976 |         assert simplified["customfield_10001"]["name"] == "My Custom Text Field"
 977 |         assert "customfield_10002" in simplified
 978 |         assert simplified["customfield_10002"]["value"] == "Custom Select Value"
 979 |         assert simplified["customfield_10002"]["name"] == "My Custom Select"
 980 |         assert "customfield_10003" in simplified
 981 | 
 982 |         issue_specific = JiraIssue.from_api_response(
 983 |             jira_issue_data, requested_fields="key,customfield_10014"
 984 |         )
 985 |         simplified_specific = issue_specific.to_simplified_dict()
 986 |         assert "customfield_10014" in simplified_specific
 987 |         assert simplified_specific.get("customfield_10014") == {
 988 |             "value": "EPIC-KEY-1",
 989 |             "name": "Epic Link",
 990 |         }
 991 | 
 992 |     def test_jira_issue_with_default_fields(self, jira_issue_data):
 993 |         """Test that JiraIssue returns only essential fields by default."""
 994 |         issue = JiraIssue.from_api_response(jira_issue_data)
 995 |         simplified = issue.to_simplified_dict()
 996 |         # Check essential fields ARE present
 997 |         essential_keys = {
 998 |             "id",
 999 |             "key",
1000 |             "summary",
1001 |             "url",
1002 |             "description",
1003 |             "status",
1004 |             "issue_type",
1005 |             "priority",
1006 |             "project",
1007 |             "resolution",
1008 |             "duedate",
1009 |             "resolutiondate",
1010 |             "parent",
1011 |             "subtasks",
1012 |             "security",
1013 |             "worklog",
1014 |             "assignee",
1015 |             "reporter",
1016 |             "labels",
1017 |             "components",
1018 |             "fix_versions",
1019 |             "epic_key",
1020 |             "epic_name",
1021 |             "timetracking",
1022 |             "created",
1023 |             "updated",
1024 |             "comments",
1025 |             "attachments",
1026 |         }
1027 |         # We check if the key is present; value might be None if not in source data
1028 |         for key in essential_keys:
1029 |             assert key in simplified, (
1030 |                 f"Essential key '{key}' missing from default simplified dict"
1031 |             )
1032 |         assert "customfield_10001" not in simplified
1033 |         assert "customfield_10002" not in simplified
1034 | 
1035 |         issue = JiraIssue.from_api_response(jira_issue_data, requested_fields="*all")
1036 |         simplified = issue.to_simplified_dict()
1037 |         assert "customfield_10001" in simplified
1038 |         assert "customfield_10002" in simplified
1039 | 
1040 |     def test_timetracking_field_processing(self, jira_issue_data):
1041 |         """Test that timetracking data is properly processed."""
1042 |         issue = JiraIssue.from_api_response(jira_issue_data)
1043 |         assert issue.timetracking is not None
1044 |         assert issue.timetracking.original_estimate == "1d"
1045 |         assert issue.timetracking.remaining_estimate == "4h"
1046 |         assert issue.timetracking.time_spent == "4h"
1047 |         assert issue.timetracking.original_estimate_seconds == 28800
1048 |         assert issue.timetracking.remaining_estimate_seconds == 14400
1049 |         assert issue.timetracking.time_spent_seconds == 14400
1050 | 
1051 |         issue.requested_fields = "*all"
1052 |         simplified = issue.to_simplified_dict()
1053 |         assert "timetracking" in simplified
1054 |         assert simplified["timetracking"]["original_estimate"] == "1d"
1055 | 
1056 |         issue.requested_fields = ["summary", "timetracking"]
1057 |         simplified = issue.to_simplified_dict()
1058 |         assert "timetracking" in simplified
1059 |         assert simplified["timetracking"]["original_estimate"] == "1d"
1060 | 
1061 | 
1062 | class TestJiraSearchResult:
1063 |     """Tests for the JiraSearchResult model."""
1064 | 
1065 |     def test_from_api_response_with_valid_data(self, jira_search_data):
1066 |         """Test creating a JiraSearchResult from valid API data."""
1067 |         search_result = JiraSearchResult.from_api_response(jira_search_data)
1068 |         assert search_result.total == 34
1069 |         assert search_result.start_at == 0
1070 |         assert search_result.max_results == 5
1071 |         assert len(search_result.issues) == 1
1072 | 
1073 |         issue = search_result.issues[0]
1074 |         assert isinstance(issue, JiraIssue)
1075 |         assert issue.key == "PROJ-123"
1076 |         assert issue.summary == "Test Issue Summary"
1077 | 
1078 |     def test_from_api_response_with_empty_data(self):
1079 |         """Test creating a JiraSearchResult from empty data."""
1080 |         result = JiraSearchResult.from_api_response({})
1081 |         assert result.total == 0
1082 |         assert result.start_at == 0
1083 |         assert result.max_results == 0
1084 |         assert result.issues == []
1085 | 
1086 |     def test_from_api_response_missing_metadata(self, jira_search_data):
1087 |         """Test creating a JiraSearchResult when API is missing metadata."""
1088 |         # Remove total, startAt, maxResults from mock data
1089 |         api_data = dict(jira_search_data)
1090 |         api_data.pop("total", None)
1091 |         api_data.pop("startAt", None)
1092 |         api_data.pop("maxResults", None)
1093 | 
1094 |         search_result = JiraSearchResult.from_api_response(api_data)
1095 |         # Verify that -1 is used for missing metadata
1096 |         assert search_result.total == -1
1097 |         assert search_result.start_at == -1
1098 |         assert search_result.max_results == -1
1099 |         assert len(search_result.issues) == 1  # Assuming mock data has issues
1100 | 
1101 |     def test_to_simplified_dict(self, jira_search_data):
1102 |         """Test converting JiraSearchResult to a simplified dictionary."""
1103 |         search_result = JiraSearchResult.from_api_response(jira_search_data)
1104 |         simplified = search_result.to_simplified_dict()
1105 | 
1106 |         # Verify the structure and basic metadata
1107 |         assert isinstance(simplified, dict)
1108 |         assert "total" in simplified
1109 |         assert "start_at" in simplified
1110 |         assert "max_results" in simplified
1111 |         assert "issues" in simplified
1112 | 
1113 |         # Verify metadata values
1114 |         assert simplified["total"] == 34
1115 |         assert simplified["start_at"] == 0
1116 |         assert simplified["max_results"] == 5
1117 | 
1118 |         # Verify issues array
1119 |         assert isinstance(simplified["issues"], list)
1120 |         assert len(simplified["issues"]) == 1
1121 | 
1122 |         # Verify that each issue is a simplified dict (not a JiraIssue object)
1123 |         issue = simplified["issues"][0]
1124 |         assert isinstance(issue, dict)
1125 |         assert issue["key"] == "PROJ-123"
1126 |         assert issue["summary"] == "Test Issue Summary"
1127 | 
1128 |         # Verify that the issues are properly simplified (calling to_simplified_dict on each)
1129 |         # This ensures field filtering works properly
1130 |         assert "id" in issue  # ID is included in simplified version
1131 |         assert "expand" not in issue  # Should be filtered out in simplified version
1132 | 
1133 |         # Verify that issue contains expected fields
1134 |         assert "assignee" in issue
1135 |         assert "created" in issue
1136 |         assert "updated" in issue
1137 | 
1138 |     def test_to_simplified_dict_empty_result(self):
1139 |         """Test converting an empty JiraSearchResult to a simplified dictionary."""
1140 |         search_result = JiraSearchResult()
1141 |         simplified = search_result.to_simplified_dict()
1142 | 
1143 |         assert isinstance(simplified, dict)
1144 |         assert simplified["total"] == 0
1145 |         assert simplified["start_at"] == 0
1146 |         assert simplified["max_results"] == 0
1147 |         assert simplified["issues"] == []
1148 | 
1149 |     def test_to_simplified_dict_with_multiple_issues(self):
1150 |         """Test converting JiraSearchResult with multiple issues to a simplified dictionary."""
1151 |         # Create mock data with multiple issues
1152 |         mock_data = {
1153 |             "total": 2,
1154 |             "startAt": 0,
1155 |             "maxResults": 10,
1156 |             "issues": [
1157 |                 {
1158 |                     "id": "12345",
1159 |                     "key": "PROJ-123",
1160 |                     "fields": {
1161 |                         "summary": "First Issue",
1162 |                         "status": {"name": "In Progress"},
1163 |                     },
1164 |                 },
1165 |                 {
1166 |                     "id": "12346",
1167 |                     "key": "PROJ-124",
1168 |                     "fields": {
1169 |                         "summary": "Second Issue",
1170 |                         "status": {"name": "Done"},
1171 |                     },
1172 |                 },
1173 |             ],
1174 |         }
1175 | 
1176 |         search_result = JiraSearchResult.from_api_response(mock_data)
1177 |         simplified = search_result.to_simplified_dict()
1178 | 
1179 |         # Verify metadata
1180 |         assert simplified["total"] == 2
1181 |         assert simplified["start_at"] == 0
1182 |         assert simplified["max_results"] == 10
1183 | 
1184 |         # Verify issues
1185 |         assert len(simplified["issues"]) == 2
1186 |         assert simplified["issues"][0]["key"] == "PROJ-123"
1187 |         assert simplified["issues"][0]["summary"] == "First Issue"
1188 |         assert simplified["issues"][1]["key"] == "PROJ-124"
1189 |         assert simplified["issues"][1]["summary"] == "Second Issue"
1190 | 
1191 | 
1192 | class TestJiraProject:
1193 |     """Tests for the JiraProject model."""
1194 | 
1195 |     def test_from_api_response_with_valid_data(self):
1196 |         """Test creating a JiraProject from valid API data."""
1197 |         project_data = {
1198 |             "id": "10000",
1199 |             "key": "TEST",
1200 |             "name": "Test Project",
1201 |             "description": "This is a test project",
1202 |             "lead": {
1203 |                 "accountId": "5b10a2844c20165700ede21g",
1204 |                 "displayName": "John Doe",
1205 |                 "active": True,
1206 |             },
1207 |             "self": "https://example.atlassian.net/rest/api/3/project/10000",
1208 |             "projectCategory": {
1209 |                 "id": "10100",
1210 |                 "name": "Software Projects",
1211 |                 "description": "Software development projects",
1212 |             },
1213 |             "avatarUrls": {
1214 |                 "48x48": "https://example.atlassian.net/secure/projectavatar?pid=10000&avatarId=10011",
1215 |                 "24x24": "https://example.atlassian.net/secure/projectavatar?pid=10000&size=small&avatarId=10011",
1216 |             },
1217 |         }
1218 |         project = JiraProject.from_api_response(project_data)
1219 |         assert project.id == "10000"
1220 |         assert project.key == "TEST"
1221 |         assert project.name == "Test Project"
1222 |         assert project.description == "This is a test project"
1223 |         assert project.lead is not None
1224 |         assert project.lead.display_name == "John Doe"
1225 |         assert project.url == "https://example.atlassian.net/rest/api/3/project/10000"
1226 |         assert project.category_name == "Software Projects"
1227 |         assert (
1228 |             project.avatar_url
1229 |             == "https://example.atlassian.net/secure/projectavatar?pid=10000&avatarId=10011"
1230 |         )
1231 | 
1232 |     def test_from_api_response_with_empty_data(self):
1233 |         """Test creating a JiraProject from empty data."""
1234 |         project = JiraProject.from_api_response({})
1235 |         assert project.id == JIRA_DEFAULT_PROJECT
1236 |         assert project.key == EMPTY_STRING
1237 |         assert project.name == UNKNOWN
1238 |         assert project.description is None
1239 |         assert project.lead is None
1240 |         assert project.url is None
1241 |         assert project.category_name is None
1242 |         assert project.avatar_url is None
1243 | 
1244 |     def test_to_simplified_dict(self):
1245 |         """Test converting a JiraProject to a simplified dictionary."""
1246 |         project_data = {
1247 |             "id": "10000",
1248 |             "key": "TEST",
1249 |             "name": "Test Project",
1250 |             "description": "This is a test project",
1251 |             "lead": {
1252 |                 "accountId": "5b10a2844c20165700ede21g",
1253 |                 "displayName": "John Doe",
1254 |                 "active": True,
1255 |             },
1256 |             "self": "https://example.atlassian.net/rest/api/3/project/10000",
1257 |             "projectCategory": {
1258 |                 "name": "Software Projects",
1259 |             },
1260 |         }
1261 |         project = JiraProject.from_api_response(project_data)
1262 |         simplified = project.to_simplified_dict()
1263 |         assert simplified["key"] == "TEST"
1264 |         assert simplified["name"] == "Test Project"
1265 |         assert simplified["description"] == "This is a test project"
1266 |         assert simplified["lead"] is not None
1267 |         assert simplified["lead"]["display_name"] == "John Doe"
1268 |         assert simplified["category"] == "Software Projects"
1269 |         assert "id" not in simplified
1270 |         assert "url" not in simplified
1271 |         assert "avatar_url" not in simplified
1272 | 
1273 | 
1274 | class TestJiraTransition:
1275 |     """Tests for the JiraTransition model."""
1276 | 
1277 |     def test_from_api_response_with_valid_data(self):
1278 |         """Test creating a JiraTransition from valid API data."""
1279 |         transition_data = {
1280 |             "id": "10",
1281 |             "name": "Start Progress",
1282 |             "to": {
1283 |                 "id": "3",
1284 |                 "name": "In Progress",
1285 |                 "statusCategory": {
1286 |                     "id": 4,
1287 |                     "key": "indeterminate",
1288 |                     "name": "In Progress",
1289 |                     "colorName": "yellow",
1290 |                 },
1291 |             },
1292 |             "hasScreen": True,
1293 |             "isGlobal": False,
1294 |             "isInitial": False,
1295 |             "isConditional": True,
1296 |         }
1297 |         transition = JiraTransition.from_api_response(transition_data)
1298 |         assert transition.id == "10"
1299 |         assert transition.name == "Start Progress"
1300 |         assert transition.to_status is not None
1301 |         assert transition.to_status.id == "3"
1302 |         assert transition.to_status.name == "In Progress"
1303 |         assert transition.to_status.category is not None
1304 |         assert transition.to_status.category.name == "In Progress"
1305 |         assert transition.has_screen is True
1306 |         assert transition.is_global is False
1307 |         assert transition.is_initial is False
1308 |         assert transition.is_conditional is True
1309 | 
1310 |     def test_from_api_response_with_empty_data(self):
1311 |         """Test creating a JiraTransition from empty data."""
1312 |         transition = JiraTransition.from_api_response({})
1313 |         assert transition.id == JIRA_DEFAULT_ID
1314 |         assert transition.name == EMPTY_STRING
1315 |         assert transition.to_status is None
1316 |         assert transition.has_screen is False
1317 |         assert transition.is_global is False
1318 |         assert transition.is_initial is False
1319 |         assert transition.is_conditional is False
1320 | 
1321 |     def test_to_simplified_dict(self):
1322 |         """Test converting a JiraTransition to a simplified dictionary."""
1323 |         transition_data = {
1324 |             "id": "10",
1325 |             "name": "Start Progress",
1326 |             "to": {
1327 |                 "id": "3",
1328 |                 "name": "In Progress",
1329 |                 "statusCategory": {
1330 |                     "id": 4,
1331 |                     "key": "indeterminate",
1332 |                     "name": "In Progress",
1333 |                     "colorName": "yellow",
1334 |                 },
1335 |             },
1336 |             "hasScreen": True,
1337 |         }
1338 |         transition = JiraTransition.from_api_response(transition_data)
1339 |         simplified = transition.to_simplified_dict()
1340 |         assert simplified["id"] == "10"
1341 |         assert simplified["name"] == "Start Progress"
1342 |         assert simplified["to_status"] is not None
1343 |         assert simplified["to_status"]["name"] == "In Progress"
1344 |         assert "has_screen" not in simplified
1345 |         assert "is_global" not in simplified
1346 | 
1347 | 
1348 | class TestJiraIssueLinkType:
1349 |     """Tests for the JiraIssueLinkType model."""
1350 | 
1351 |     def test_from_api_response_with_valid_data(self):
1352 |         """Test creating a JiraIssueLinkType from valid API data."""
1353 |         data = {
1354 |             "id": "10001",
1355 |             "name": "Blocks",
1356 |             "inward": "is blocked by",
1357 |             "outward": "blocks",
1358 |             "self": "https://example.atlassian.net/rest/api/3/issueLinkType/10001",
1359 |         }
1360 |         link_type = JiraIssueLinkType.from_api_response(data)
1361 |         assert link_type.id == "10001"
1362 |         assert link_type.name == "Blocks"
1363 |         assert link_type.inward == "is blocked by"
1364 |         assert link_type.outward == "blocks"
1365 |         assert (
1366 |             link_type.self_url
1367 |             == "https://example.atlassian.net/rest/api/3/issueLinkType/10001"
1368 |         )
1369 | 
1370 |     def test_from_api_response_with_empty_data(self):
1371 |         """Test creating a JiraIssueLinkType from empty data."""
1372 |         link_type = JiraIssueLinkType.from_api_response({})
1373 |         assert link_type.id == JIRA_DEFAULT_ID
1374 |         assert link_type.name == UNKNOWN
1375 |         assert link_type.inward == EMPTY_STRING
1376 |         assert link_type.outward == EMPTY_STRING
1377 |         assert link_type.self_url is None
1378 | 
1379 |     def test_from_api_response_with_none_data(self):
1380 |         """Test creating a JiraIssueLinkType from None data."""
1381 |         link_type = JiraIssueLinkType.from_api_response(None)
1382 |         assert link_type.id == JIRA_DEFAULT_ID
1383 |         assert link_type.name == UNKNOWN
1384 |         assert link_type.inward == EMPTY_STRING
1385 |         assert link_type.outward == EMPTY_STRING
1386 |         assert link_type.self_url is None
1387 | 
1388 |     def test_to_simplified_dict(self):
1389 |         """Test converting JiraIssueLinkType to a simplified dictionary."""
1390 |         link_type = JiraIssueLinkType(
1391 |             id="10001",
1392 |             name="Blocks",
1393 |             inward="is blocked by",
1394 |             outward="blocks",
1395 |             self_url="https://example.atlassian.net/rest/api/3/issueLinkType/10001",
1396 |         )
1397 |         simplified = link_type.to_simplified_dict()
1398 |         assert isinstance(simplified, dict)
1399 |         assert simplified["id"] == "10001"
1400 |         assert simplified["name"] == "Blocks"
1401 |         assert simplified["inward"] == "is blocked by"
1402 |         assert simplified["outward"] == "blocks"
1403 |         assert "self" in simplified
1404 |         assert (
1405 |             simplified["self"]
1406 |             == "https://example.atlassian.net/rest/api/3/issueLinkType/10001"
1407 |         )
1408 | 
1409 | 
1410 | class TestJiraLinkedIssueFields:
1411 |     """Tests for the JiraLinkedIssueFields model."""
1412 | 
1413 |     def test_from_api_response_with_valid_data(self):
1414 |         """Test creating a JiraLinkedIssueFields from valid API data."""
1415 |         data = {
1416 |             "summary": "Linked Issue Summary",
1417 |             "status": {
1418 |                 "id": "10000",
1419 |                 "name": "In Progress",
1420 |                 "statusCategory": {
1421 |                     "id": 4,
1422 |                     "key": "indeterminate",
1423 |                     "name": "In Progress",
1424 |                     "colorName": "yellow",
1425 |                 },
1426 |             },
1427 |             "priority": {
1428 |                 "id": "3",
1429 |                 "name": "Medium",
1430 |                 "description": "Medium priority",
1431 |                 "iconUrl": "https://example.com/medium-priority.png",
1432 |             },
1433 |             "issuetype": {
1434 |                 "id": "10000",
1435 |                 "name": "Task",
1436 |                 "description": "A task that needs to be done.",
1437 |                 "iconUrl": "https://example.com/task-icon.png",
1438 |             },
1439 |         }
1440 |         fields = JiraLinkedIssueFields.from_api_response(data)
1441 |         assert fields.summary == "Linked Issue Summary"
1442 |         assert fields.status is not None
1443 |         assert fields.status.name == "In Progress"
1444 |         assert fields.priority is not None
1445 |         assert fields.priority.name == "Medium"
1446 |         assert fields.issuetype is not None
1447 |         assert fields.issuetype.name == "Task"
1448 | 
1449 |     def test_from_api_response_with_empty_data(self):
1450 |         """Test creating a JiraLinkedIssueFields from empty data."""
1451 |         fields = JiraLinkedIssueFields.from_api_response({})
1452 |         assert fields.summary == EMPTY_STRING
1453 |         assert fields.status is None
1454 |         assert fields.priority is None
1455 |         assert fields.issuetype is None
1456 | 
1457 |     def test_to_simplified_dict(self):
1458 |         """Test converting JiraLinkedIssueFields to a simplified dictionary."""
1459 |         fields = JiraLinkedIssueFields(
1460 |             summary="Linked Issue Summary",
1461 |             status=JiraStatus(name="In Progress"),
1462 |             priority=JiraPriority(name="Medium"),
1463 |             issuetype=JiraIssueType(name="Task"),
1464 |         )
1465 |         simplified = fields.to_simplified_dict()
1466 |         assert simplified["summary"] == "Linked Issue Summary"
1467 |         assert simplified["status"]["name"] == "In Progress"
1468 |         assert simplified["priority"]["name"] == "Medium"
1469 |         assert simplified["issuetype"]["name"] == "Task"
1470 | 
1471 | 
1472 | class TestJiraLinkedIssue:
1473 |     """Tests for the JiraLinkedIssue model."""
1474 | 
1475 |     def test_from_api_response_with_valid_data(self):
1476 |         """Test creating a JiraLinkedIssue from valid API data."""
1477 |         data = {
1478 |             "id": "10001",
1479 |             "key": "PROJ-456",
1480 |             "self": "https://example.atlassian.net/rest/api/2/issue/10001",
1481 |             "fields": {
1482 |                 "summary": "Linked Issue Summary",
1483 |                 "status": {
1484 |                     "id": "10000",
1485 |                     "name": "In Progress",
1486 |                 },
1487 |                 "priority": {
1488 |                     "id": "3",
1489 |                     "name": "Medium",
1490 |                 },
1491 |                 "issuetype": {
1492 |                     "id": "10000",
1493 |                     "name": "Task",
1494 |                 },
1495 |             },
1496 |         }
1497 |         linked_issue = JiraLinkedIssue.from_api_response(data)
1498 |         assert linked_issue.id == "10001"
1499 |         assert linked_issue.key == "PROJ-456"
1500 |         assert (
1501 |             linked_issue.self_url
1502 |             == "https://example.atlassian.net/rest/api/2/issue/10001"
1503 |         )
1504 |         assert linked_issue.fields is not None
1505 |         assert linked_issue.fields.summary == "Linked Issue Summary"
1506 |         assert linked_issue.fields.status is not None
1507 |         assert linked_issue.fields.status.name == "In Progress"
1508 | 
1509 |     def test_from_api_response_with_empty_data(self):
1510 |         """Test creating a JiraLinkedIssue from empty data."""
1511 |         linked_issue = JiraLinkedIssue.from_api_response({})
1512 |         assert linked_issue.id == JIRA_DEFAULT_ID
1513 |         assert linked_issue.key == EMPTY_STRING
1514 |         assert linked_issue.self_url is None
1515 |         assert linked_issue.fields is None
1516 | 
1517 |     def test_to_simplified_dict(self):
1518 |         """Test converting JiraLinkedIssue to a simplified dictionary."""
1519 |         linked_issue = JiraLinkedIssue(
1520 |             id="10001",
1521 |             key="PROJ-456",
1522 |             self_url="https://example.atlassian.net/rest/api/2/issue/10001",
1523 |             fields=JiraLinkedIssueFields(
1524 |                 summary="Linked Issue Summary",
1525 |                 status=JiraStatus(name="In Progress"),
1526 |                 priority=JiraPriority(name="Medium"),
1527 |                 issuetype=JiraIssueType(name="Task"),
1528 |             ),
1529 |         )
1530 |         simplified = linked_issue.to_simplified_dict()
1531 |         assert simplified["id"] == "10001"
1532 |         assert simplified["key"] == "PROJ-456"
1533 |         assert (
1534 |             simplified["self"] == "https://example.atlassian.net/rest/api/2/issue/10001"
1535 |         )
1536 |         assert simplified["fields"]["summary"] == "Linked Issue Summary"
1537 |         assert simplified["fields"]["status"]["name"] == "In Progress"
1538 | 
1539 | 
1540 | class TestJiraIssueLink:
1541 |     """Tests for the JiraIssueLink model."""
1542 | 
1543 |     def test_from_api_response_with_valid_data(self):
1544 |         """Test creating a JiraIssueLink from valid API data."""
1545 |         data = {
1546 |             "id": "10001",
1547 |             "type": {
1548 |                 "id": "10000",
1549 |                 "name": "Blocks",
1550 |                 "inward": "is blocked by",
1551 |                 "outward": "blocks",
1552 |                 "self": "https://example.atlassian.net/rest/api/2/issueLinkType/10000",
1553 |             },
1554 |             "inwardIssue": {
1555 |                 "id": "10002",
1556 |                 "key": "PROJ-789",
1557 |                 "self": "https://example.atlassian.net/rest/api/2/issue/10002",
1558 |                 "fields": {
1559 |                     "summary": "Inward Issue Summary",
1560 |                     "status": {
1561 |                         "id": "10000",
1562 |                         "name": "In Progress",
1563 |                     },
1564 |                 },
1565 |             },
1566 |         }
1567 |         issue_link = JiraIssueLink.from_api_response(data)
1568 |         assert issue_link.id == "10001"
1569 |         assert issue_link.type is not None
1570 |         assert issue_link.type.name == "Blocks"
1571 |         assert issue_link.inward_issue is not None
1572 |         assert issue_link.inward_issue.key == "PROJ-789"
1573 |         assert issue_link.outward_issue is None
1574 | 
1575 |     def test_from_api_response_with_outward_issue(self):
1576 |         """Test creating a JiraIssueLink with an outward issue."""
1577 |         data = {
1578 |             "id": "10001",
1579 |             "type": {
1580 |                 "id": "10000",
1581 |                 "name": "Blocks",
1582 |                 "inward": "is blocked by",
1583 |                 "outward": "blocks",
1584 |             },
1585 |             "outwardIssue": {
1586 |                 "id": "10003",
1587 |                 "key": "PROJ-101",
1588 |                 "fields": {
1589 |                     "summary": "Outward Issue Summary",
1590 |                     "status": {
1591 |                         "id": "10000",
1592 |                         "name": "In Progress",
1593 |                     },
1594 |                 },
1595 |             },
1596 |         }
1597 |         issue_link = JiraIssueLink.from_api_response(data)
1598 |         assert issue_link.id == "10001"
1599 |         assert issue_link.type is not None
1600 |         assert issue_link.type.name == "Blocks"
1601 |         assert issue_link.inward_issue is None
1602 |         assert issue_link.outward_issue is not None
1603 |         assert issue_link.outward_issue.key == "PROJ-101"
1604 | 
1605 |     def test_from_api_response_with_empty_data(self):
1606 |         """Test creating a JiraIssueLink from empty data."""
1607 |         issue_link = JiraIssueLink.from_api_response({})
1608 |         assert issue_link.id == JIRA_DEFAULT_ID
1609 |         assert issue_link.type is None
1610 |         assert issue_link.inward_issue is None
1611 |         assert issue_link.outward_issue is None
1612 | 
1613 |     def test_to_simplified_dict(self):
1614 |         """Test converting JiraIssueLink to a simplified dictionary."""
1615 |         issue_link = JiraIssueLink(
1616 |             id="10001",
1617 |             type=JiraIssueLinkType(
1618 |                 id="10000",
1619 |                 name="Blocks",
1620 |                 inward="is blocked by",
1621 |                 outward="blocks",
1622 |             ),
1623 |             inward_issue=JiraLinkedIssue(
1624 |                 id="10002",
1625 |                 key="PROJ-789",
1626 |                 fields=JiraLinkedIssueFields(
1627 |                     summary="Inward Issue Summary",
1628 |                     status=JiraStatus(name="In Progress"),
1629 |                 ),
1630 |             ),
1631 |         )
1632 |         simplified = issue_link.to_simplified_dict()
1633 |         assert simplified["id"] == "10001"
1634 |         assert simplified["type"]["name"] == "Blocks"
1635 |         assert simplified["inward_issue"]["key"] == "PROJ-789"
1636 |         assert "outward_issue" not in simplified
1637 | 
1638 | 
1639 | class TestJiraWorklog:
1640 |     """Tests for the JiraWorklog model."""
1641 | 
1642 |     def test_from_api_response_with_valid_data(self):
1643 |         """Test creating a JiraWorklog from valid API data."""
1644 |         worklog_data = {
1645 |             "id": "100023",
1646 |             "author": {
1647 |                 "accountId": "5b10a2844c20165700ede21g",
1648 |                 "displayName": "John Doe",
1649 |                 "active": True,
1650 |             },
1651 |             "comment": "Worked on the issue today",
1652 |             "created": "2023-05-01T10:00:00.000+0000",
1653 |             "updated": "2023-05-01T10:30:00.000+0000",
1654 |             "started": "2023-05-01T09:00:00.000+0000",
1655 |             "timeSpent": "2h 30m",
1656 |             "timeSpentSeconds": 9000,
1657 |         }
1658 |         worklog = JiraWorklog.from_api_response(worklog_data)
1659 |         assert worklog.id == "100023"
1660 |         assert worklog.author is not None
1661 |         assert worklog.author.display_name == "John Doe"
1662 |         assert worklog.comment == "Worked on the issue today"
1663 |         assert worklog.created == "2023-05-01T10:00:00.000+0000"
1664 |         assert worklog.updated == "2023-05-01T10:30:00.000+0000"
1665 |         assert worklog.started == "2023-05-01T09:00:00.000+0000"
1666 |         assert worklog.time_spent == "2h 30m"
1667 |         assert worklog.time_spent_seconds == 9000
1668 | 
1669 |     def test_from_api_response_with_empty_data(self):
1670 |         """Test creating a JiraWorklog from empty data."""
1671 |         worklog = JiraWorklog.from_api_response({})
1672 |         assert worklog.id == JIRA_DEFAULT_ID
1673 |         assert worklog.author is None
1674 |         assert worklog.comment is None
1675 |         assert worklog.created == EMPTY_STRING
1676 |         assert worklog.updated == EMPTY_STRING
1677 |         assert worklog.started == EMPTY_STRING
1678 |         assert worklog.time_spent == EMPTY_STRING
1679 |         assert worklog.time_spent_seconds == 0
1680 | 
1681 |     def test_to_simplified_dict(self):
1682 |         """Test converting a JiraWorklog to a simplified dictionary."""
1683 |         worklog_data = {
1684 |             "id": "100023",
1685 |             "author": {
1686 |                 "accountId": "5b10a2844c20165700ede21g",
1687 |                 "displayName": "John Doe",
1688 |                 "active": True,
1689 |             },
1690 |             "comment": "Worked on the issue today",
1691 |             "created": "2023-05-01T10:00:00.000+0000",
1692 |             "updated": "2023-05-01T10:30:00.000+0000",
1693 |             "started": "2023-05-01T09:00:00.000+0000",
1694 |             "timeSpent": "2h 30m",
1695 |             "timeSpentSeconds": 9000,
1696 |         }
1697 |         worklog = JiraWorklog.from_api_response(worklog_data)
1698 |         simplified = worklog.to_simplified_dict()
1699 |         assert simplified["time_spent"] == "2h 30m"
1700 |         assert simplified["time_spent_seconds"] == 9000
1701 |         assert simplified["author"] is not None
1702 |         assert simplified["author"]["display_name"] == "John Doe"
1703 |         assert simplified["comment"] == "Worked on the issue today"
1704 |         assert "created" in simplified
1705 |         assert "updated" in simplified
1706 |         assert "started" in simplified
1707 | 
1708 | 
1709 | class TestRealJiraData:
1710 |     """Tests using real Jira data (optional)."""
1711 | 
1712 |     # Helper to get client/config
1713 |     def _get_client(self) -> IssuesMixin | None:
1714 |         if not real_api_available:
1715 |             return None
1716 |         try:
1717 |             config = JiraConfig.from_env()
1718 |             return JiraFetcher(config=config)
1719 |         except ValueError:
1720 |             pytest.skip("Real Jira environment not configured")
1721 |             return None
1722 | 
1723 |     def _get_project_client(self) -> ProjectsMixin | None:
1724 |         if not real_api_available:
1725 |             return None
1726 |         try:
1727 |             config = JiraConfig.from_env()
1728 | 
1729 |             return JiraFetcher(config=config)
1730 |         except ValueError:
1731 |             pytest.skip("Real Jira environment not configured")
1732 |             return None
1733 | 
1734 |     def _get_transition_client(self) -> TransitionsMixin | None:
1735 |         if not real_api_available:
1736 |             return None
1737 |         try:
1738 |             config = JiraConfig.from_env()
1739 |             return JiraFetcher(config=config)
1740 |         except ValueError:
1741 |             pytest.skip("Real Jira environment not configured")
1742 |             return None
1743 | 
1744 |     def _get_worklog_client(self) -> WorklogMixin | None:
1745 |         if not real_api_available:
1746 |             return None
1747 |         try:
1748 |             config = JiraConfig.from_env()
1749 |             return JiraFetcher(config=config)
1750 |         except ValueError:
1751 |             pytest.skip("Real Jira environment not configured")
1752 |             return None
1753 | 
1754 |     def _get_base_jira_client(self) -> Jira | None:
1755 |         if not real_api_available:
1756 |             return None
1757 |         try:
1758 |             config = JiraConfig.from_env()
1759 |             if config.auth_type == "basic":
1760 |                 return Jira(
1761 |                     url=config.url,
1762 |                     username=config.username,
1763 |                     password=config.api_token,
1764 |                     cloud=config.is_cloud,
1765 |                 )
1766 |             else:  # token
1767 |                 return Jira(
1768 |                     url=config.url, token=config.personal_token, cloud=config.is_cloud
1769 |                 )
1770 |         except ValueError:
1771 |             pytest.skip("Real Jira environment not configured")
1772 |             return None
1773 | 
1774 |     def test_real_jira_issue(self, use_real_jira_data, default_jira_issue_key):
1775 |         """Test that the JiraIssue model works with real Jira API data."""
1776 |         if not use_real_jira_data:
1777 |             pytest.skip("Skipping real Jira data test")
1778 |         issues_client = self._get_client()
1779 |         if not issues_client or not default_jira_issue_key:
1780 |             pytest.skip("Real Jira client/issue key not available")
1781 | 
1782 |         try:
1783 |             issue = issues_client.get_issue(default_jira_issue_key)
1784 |             assert isinstance(issue, JiraIssue)
1785 |             assert issue.key == default_jira_issue_key
1786 |             assert issue.id is not None
1787 |             assert issue.summary is not None
1788 | 
1789 |             assert hasattr(issue, "project")
1790 |             assert issue.project is None or isinstance(issue.project, JiraProject)
1791 |             assert hasattr(issue, "resolution")
1792 |             assert issue.resolution is None or isinstance(
1793 |                 issue.resolution, JiraResolution
1794 |             )
1795 |             assert hasattr(issue, "duedate")
1796 |             assert issue.duedate is None or isinstance(issue.duedate, str)
1797 |             assert hasattr(issue, "resolutiondate")
1798 |             assert issue.resolutiondate is None or isinstance(issue.resolutiondate, str)
1799 |             assert hasattr(issue, "parent")
1800 |             assert issue.parent is None or isinstance(issue.parent, dict)
1801 |             assert hasattr(issue, "subtasks")
1802 |             assert isinstance(issue.subtasks, list)
1803 |             if issue.subtasks:
1804 |                 assert isinstance(issue.subtasks[0], dict)
1805 |             assert hasattr(issue, "security")
1806 |             assert issue.security is None or isinstance(issue.security, dict)
1807 |             assert hasattr(issue, "worklog")
1808 |             assert issue.worklog is None or isinstance(issue.worklog, dict)
1809 | 
1810 |             simplified = issue.to_simplified_dict()
1811 |             assert simplified["key"] == default_jira_issue_key
1812 |         except Exception as e:
1813 |             pytest.fail(f"Error testing real Jira issue: {e}")
1814 | 
1815 |     def test_real_jira_project(self, use_real_jira_data):
1816 |         """Test that the JiraProject model works with real Jira API data."""
1817 |         if not use_real_jira_data:
1818 |             pytest.skip("Skipping real Jira data test")
1819 |         projects_client = self._get_project_client()
1820 |         if not projects_client:
1821 |             pytest.skip("Real Jira client not available")
1822 | 
1823 |         # Check for JIRA_TEST_ISSUE_KEY explicitly
1824 |         if not os.environ.get("JIRA_TEST_ISSUE_KEY"):
1825 |             pytest.skip("JIRA_TEST_ISSUE_KEY environment variable not set")
1826 | 
1827 |         default_issue_key = os.environ.get("JIRA_TEST_ISSUE_KEY")
1828 |         project_key = default_issue_key.split("-")[0]
1829 | 
1830 |         if not project_key:
1831 |             pytest.skip("Could not extract project key from JIRA_TEST_ISSUE_KEY")
1832 | 
1833 |         try:
1834 |             project = projects_client.get_project_model(project_key)
1835 | 
1836 |             if project is None:
1837 |                 pytest.skip(f"Could not get project model for {project_key}")
1838 | 
1839 |             assert isinstance(project, JiraProject)
1840 |             assert project.key == project_key
1841 |             assert project.id is not None
1842 |             assert project.name is not None
1843 | 
1844 |             simplified = project.to_simplified_dict()
1845 |             assert simplified["key"] == project_key
1846 |         except (AttributeError, TypeError, ValueError) as e:
1847 |             pytest.skip(f"Error parsing project data: {e}")
1848 |         except Exception as e:
1849 |             pytest.fail(f"Error testing real Jira project: {e}")
1850 | 
1851 |     def test_real_jira_transitions(self, use_real_jira_data, default_jira_issue_key):
1852 |         """Test that the JiraTransition model works with real Jira API data."""
1853 |         if not use_real_jira_data:
1854 |             pytest.skip("Skipping real Jira data test")
1855 |         transitions_client = self._get_transition_client()
1856 |         if not transitions_client or not default_jira_issue_key:
1857 |             pytest.skip("Real Jira client/issue key not available")
1858 | 
1859 |         # Use the underlying Atlassian API client directly for raw data
1860 |         jira = self._get_base_jira_client()
1861 |         if not jira:
1862 |             pytest.skip("Base Jira client failed")
1863 | 
1864 |         transitions_data = None  # Initialize
1865 |         try:
1866 |             transitions_data = jira.get_issue_transitions(default_jira_issue_key)
1867 | 
1868 |             actual_transitions_list = []
1869 |             if isinstance(transitions_data, list):
1870 |                 actual_transitions_list = transitions_data
1871 |             else:
1872 |                 # Handle unexpected format with test failure
1873 |                 pytest.fail(
1874 |                     f"Unexpected transitions data format received from API: "
1875 |                     f"{type(transitions_data)}. Data: {transitions_data}"
1876 |                 )
1877 | 
1878 |             # Verify transitions list is actually a list
1879 |             assert isinstance(actual_transitions_list, list)
1880 | 
1881 |             if not actual_transitions_list:
1882 |                 pytest.skip(f"No transitions found for issue {default_jira_issue_key}")
1883 | 
1884 |             transition_item = actual_transitions_list[0]
1885 |             assert isinstance(transition_item, dict)
1886 | 
1887 |             # Check for essential keys in the raw data
1888 |             assert "id" in transition_item
1889 |             assert "name" in transition_item
1890 |             assert "to" in transition_item
1891 | 
1892 |             # Only check 'to' field name if it's a dictionary
1893 |             if isinstance(transition_item["to"], dict):
1894 |                 assert "name" in transition_item["to"]
1895 | 
1896 |             # Convert to model
1897 |             transition = JiraTransition.from_api_response(transition_item)
1898 |             assert isinstance(transition, JiraTransition)
1899 |             assert transition.id == str(transition_item["id"])  # Ensure ID is string
1900 |             assert transition.name == transition_item["name"]
1901 | 
1902 |             simplified = transition.to_simplified_dict()
1903 |             assert simplified["id"] == str(transition_item["id"])
1904 |             assert simplified["name"] == transition_item["name"]
1905 | 
1906 |         except Exception as e:
1907 |             # Include data type details in error message
1908 |             error_details = f"Received data type: {type(transitions_data)}"
1909 |             if transitions_data is not None:
1910 |                 error_details += (
1911 |                     f", Data: {str(transitions_data)[:200]}..."  # Show partial data
1912 |                 )
1913 | 
1914 |             pytest.fail(
1915 |                 f"Error testing real Jira transitions for issue {default_jira_issue_key}: {e}. {error_details}"
1916 |             )
1917 | 
1918 |     def test_real_jira_worklog(self, use_real_jira_data, default_jira_issue_key):
1919 |         """Test that the JiraWorklog model works with real Jira API data."""
1920 |         if not use_real_jira_data:
1921 |             pytest.skip("Skipping real Jira data test")
1922 |         worklog_client = self._get_worklog_client()
1923 |         if not worklog_client or not default_jira_issue_key:
1924 |             pytest.skip("Real Jira client/issue key not available")
1925 | 
1926 |         try:
1927 |             # Get worklogs using the model method
1928 |             worklogs = worklog_client.get_worklog_models(default_jira_issue_key)
1929 |             assert isinstance(worklogs, list)
1930 | 
1931 |             if not worklogs:
1932 |                 pytest.skip(f"Issue {default_jira_issue_key} has no worklogs to test.")
1933 | 
1934 |             # Test the first worklog
1935 |             worklog = worklogs[0]
1936 |             assert isinstance(worklog, JiraWorklog)
1937 |             assert worklog.id is not None
1938 |             assert worklog.time_spent_seconds >= 0
1939 |             if worklog.author:
1940 |                 assert isinstance(worklog.author, JiraUser)
1941 | 
1942 |             simplified = worklog.to_simplified_dict()
1943 |             assert "id" in simplified
1944 |             assert "time_spent" in simplified
1945 | 
1946 |         except Exception as e:
1947 |             pytest.fail(f"Error testing real Jira worklog: {e}")
1948 | 
```
Page 13/13FirstPrevNextLast