We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/sooperset/mcp-atlassian'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Tests for the Jira Fields mixin."""
from typing import Any
from unittest.mock import MagicMock
import pytest
from mcp_atlassian.jira import JiraFetcher
from mcp_atlassian.jira.fields import FieldsMixin
class TestFieldsMixin:
"""Tests for the FieldsMixin class."""
@pytest.fixture
def fields_mixin(self, jira_fetcher: JiraFetcher) -> FieldsMixin:
"""Create a FieldsMixin instance with mocked dependencies."""
mixin = jira_fetcher
mixin._field_ids_cache = None
return mixin
@pytest.fixture
def mock_fields(self):
"""Return mock field data."""
return [
{"id": "summary", "name": "Summary", "schema": {"type": "string"}},
{"id": "description", "name": "Description", "schema": {"type": "string"}},
{"id": "status", "name": "Status", "schema": {"type": "status"}},
{"id": "assignee", "name": "Assignee", "schema": {"type": "user"}},
{
"id": "customfield_10010",
"name": "Epic Link",
"schema": {
"type": "string",
"custom": "com.pyxis.greenhopper.jira:gh-epic-link",
},
},
{
"id": "customfield_10011",
"name": "Epic Name",
"schema": {
"type": "string",
"custom": "com.pyxis.greenhopper.jira:gh-epic-label",
},
},
{
"id": "customfield_10012",
"name": "Story Points",
"schema": {"type": "number"},
},
]
def test_get_field_ids_cache(self, fields_mixin: FieldsMixin, mock_fields):
"""Test get_fields uses cache when available."""
# Set up the cache
fields_mixin._field_ids_cache = mock_fields
# Call the method
result = fields_mixin.get_fields()
# Verify cache was used
assert result == mock_fields
fields_mixin.jira.get_all_fields.assert_not_called()
def test_get_fields_refresh(self, fields_mixin: FieldsMixin, mock_fields):
"""Test get_fields refreshes data when requested."""
# Set up the cache
fields_mixin._field_ids_cache = [{"id": "old_data", "name": "old data"}]
# Mock the API response
fields_mixin.jira.get_all_fields.return_value = mock_fields
# Call the method with refresh=True
result = fields_mixin.get_fields(refresh=True)
# Verify API was called
fields_mixin.jira.get_all_fields.assert_called_once()
assert result == mock_fields
# Verify cache was updated
assert fields_mixin._field_ids_cache == mock_fields
def test_get_fields_from_api(
self, fields_mixin: FieldsMixin, mock_fields: list[dict[str, Any]]
):
"""Test get_fields fetches from API when no cache exists."""
# Mock the API response
fields_mixin.jira.get_all_fields.return_value = mock_fields
# Call the method
result = fields_mixin.get_fields()
# Verify API was called
fields_mixin.jira.get_all_fields.assert_called_once()
assert result == mock_fields
# Verify cache was created
assert fields_mixin._field_ids_cache == mock_fields
def test_get_fields_error(self, fields_mixin: FieldsMixin):
"""Test get_fields handles errors gracefully."""
# Mock API error
fields_mixin.jira.get_all_fields.side_effect = Exception("API error")
# Call the method
result = fields_mixin.get_fields()
# Verify empty list is returned on error
assert result == []
def test_get_field_id_by_exact_match(self, fields_mixin: FieldsMixin, mock_fields):
"""Test get_field_id finds field by exact name match."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Call the method
result = fields_mixin.get_field_id("Summary")
# Verify the result
assert result == "summary"
def test_get_field_id_case_insensitive(
self, fields_mixin: FieldsMixin, mock_fields
):
"""Test get_field_id is case-insensitive."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Call the method with different case
result = fields_mixin.get_field_id("summary")
# Verify the result
assert result == "summary"
def test_get_field_id_exact_match_case_insensitive(
self, fields_mixin: FieldsMixin, mock_fields
):
"""Test get_field_id finds field by exact match (case-insensitive) using the map."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Ensure the map is generated based on the mock fields for this test
fields_mixin._generate_field_map(force_regenerate=True)
# Call the method with exact name (case-insensitive)
result = fields_mixin.get_field_id("epic link")
# Verify the result (should find Epic Link as first match)
assert result == "customfield_10010"
def test_get_field_id_not_found(self, fields_mixin: FieldsMixin, mock_fields):
"""Test get_field_id returns None when field not found."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Call the method with non-existent field
result = fields_mixin.get_field_id("NonExistent")
# Verify the result
assert result is None
def test_get_field_id_error(self, fields_mixin: FieldsMixin):
"""Test get_field_id handles errors gracefully."""
# Make get_fields raise an exception
fields_mixin.get_fields = MagicMock(
side_effect=Exception("Error getting fields")
)
# Call the method
result = fields_mixin.get_field_id("Summary")
# Verify None is returned on error
assert result is None
def test_get_field_by_id(self, fields_mixin: FieldsMixin, mock_fields):
"""Test get_field_by_id retrieves field definition correctly."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Call the method
result = fields_mixin.get_field_by_id("customfield_10012")
# Verify the result
assert result == mock_fields[6] # The Story Points field
assert result["name"] == "Story Points"
def test_get_field_by_id_not_found(self, fields_mixin: FieldsMixin, mock_fields):
"""Test get_field_by_id returns None when field not found."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Call the method with non-existent ID
result = fields_mixin.get_field_by_id("customfield_99999")
# Verify the result
assert result is None
def test_get_custom_fields(self, fields_mixin: FieldsMixin, mock_fields):
"""Test get_custom_fields returns only custom fields."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Call the method
result = fields_mixin.get_custom_fields()
# Verify the result
assert len(result) == 3
assert all(field["id"].startswith("customfield_") for field in result)
assert result[0]["name"] == "Epic Link"
assert result[1]["name"] == "Epic Name"
assert result[2]["name"] == "Story Points"
def test_get_required_fields(self, fields_mixin: FieldsMixin):
"""Test get_required_fields retrieves required fields correctly."""
# Mock the response for get_project_issue_types
mock_issue_types = [
{"id": "10001", "name": "Bug"},
{"id": "10002", "name": "Task"},
]
fields_mixin.get_project_issue_types = MagicMock(return_value=mock_issue_types)
# Mock the response for issue_createmeta_fieldtypes
# Real API returns paginated "values" array
mock_field_meta = {
"maxResults": 50,
"startAt": 0,
"total": 3,
"isLast": True,
"values": [
{
"required": True,
"schema": {"type": "string", "system": "summary"},
"name": "Summary",
"fieldId": "summary",
"autoCompleteUrl": "",
"hasDefaultValue": False,
"operations": ["set"],
"allowedValues": [],
},
{
"required": False,
"schema": {"type": "string", "system": "description"},
"name": "Description",
"fieldId": "description",
},
{
"required": True,
"schema": {"type": "string", "custom": "some.custom.type"},
"name": "Epic Link",
"fieldId": "customfield_10010",
},
],
}
fields_mixin.jira.issue_createmeta_fieldtypes.return_value = mock_field_meta
# Call the method
result = fields_mixin.get_required_fields("Bug", "TEST")
# Verify the result
assert len(result) == 2
assert "summary" in result
assert result["summary"]["required"] is True
assert "customfield_10010" in result
assert result["customfield_10010"]["required"] is True
assert "description" not in result
# Verify the correct API was called
fields_mixin.get_project_issue_types.assert_called_once_with("TEST")
fields_mixin.jira.issue_createmeta_fieldtypes.assert_called_once_with(
project="TEST", issue_type_id="10001"
)
def test_get_required_fields_not_found(self, fields_mixin: FieldsMixin):
"""Test get_required_fields handles project/issue type not found."""
# Scenario 1: Issue type not found in project
mock_issue_types = [{"id": "10002", "name": "Task"}] # "Bug" is missing
fields_mixin.get_project_issue_types = MagicMock(return_value=mock_issue_types)
fields_mixin.jira.issue_createmeta_fieldtypes = MagicMock()
# Call the method
result = fields_mixin.get_required_fields("Bug", "TEST")
# Verify issue type lookup was attempted, but field meta was not called
fields_mixin.get_project_issue_types.assert_called_once_with("TEST")
fields_mixin.jira.issue_createmeta_fieldtypes.assert_not_called()
# Verify the result
assert result == {}
def test_get_required_fields_error(self, fields_mixin: FieldsMixin):
"""Test get_required_fields handles errors gracefully."""
# Mock the response for get_project_issue_types
mock_issue_types = [
{"id": "10001", "name": "Bug"},
]
fields_mixin.get_project_issue_types = MagicMock(return_value=mock_issue_types)
# Mock issue_createmeta_fieldtypes to raise an error
fields_mixin.jira.issue_createmeta_fieldtypes.side_effect = Exception(
"API error"
)
# Call the method
result = fields_mixin.get_required_fields("Bug", "TEST")
# Verify the result
assert result == {}
# Verify the correct API was called (which then raised the error)
fields_mixin.jira.issue_createmeta_fieldtypes.assert_called_once_with(
project="TEST", issue_type_id="10001"
)
def test_get_jira_field_ids_cached(self, fields_mixin: FieldsMixin):
"""Test get_field_ids_to_epic returns cached field IDs."""
# Set up the cache
fields_mixin._field_ids_cache = [
{"id": "summary", "name": "Summary"},
{"id": "description", "name": "Description"},
]
# Call the method
result = fields_mixin.get_field_ids_to_epic()
# Verify the result
assert result == {
"Summary": "summary",
"Description": "description",
}
def test_get_jira_field_ids_from_fields(
self, fields_mixin: FieldsMixin, mock_fields: list[dict]
):
"""Test get_field_ids_to_epic extracts field IDs from field definitions."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Ensure field map is generated
fields_mixin._generate_field_map(force_regenerate=True)
# Call the method
result = fields_mixin.get_field_ids_to_epic()
# Verify that epic-specific fields are properly identified
assert "epic_link" in result
assert "Epic Link" in result
assert result["epic_link"] == "customfield_10010"
assert "epic_name" in result
assert "Epic Name" in result
assert result["epic_name"] == "customfield_10011"
def test_get_jira_field_ids_error(self, fields_mixin: FieldsMixin):
"""Test get_field_ids_to_epic handles errors gracefully."""
# Ensure no cache exists
fields_mixin._field_ids_cache = None
# Make get_fields raise an exception
fields_mixin.get_fields = MagicMock(
side_effect=Exception("Error getting fields")
)
# Call the method
result = fields_mixin.get_field_ids_to_epic()
# Verify the result
assert result == {}
def test_is_custom_field(self, fields_mixin: FieldsMixin):
"""Test is_custom_field correctly identifies custom fields."""
# Test with custom field
assert fields_mixin.is_custom_field("customfield_10010") is True
# Test with standard field
assert fields_mixin.is_custom_field("summary") is False
def test_format_field_value_user_field_cloud(
self, fields_mixin: FieldsMixin, mock_fields
):
"""Test format_field_value formats user fields correctly for Cloud."""
# Set up the mocks
fields_mixin.get_field_by_id = MagicMock(
return_value=mock_fields[3]
) # The Assignee field
fields_mixin._get_account_id = MagicMock(return_value="account123")
fields_mixin.config = MagicMock()
fields_mixin.config.is_cloud = True
# Call the method with a user field and string value
result = fields_mixin.format_field_value("assignee", "johndoe")
# Verify the result — Cloud uses accountId
assert result == {"accountId": "account123"}
fields_mixin._get_account_id.assert_called_once_with("johndoe")
def test_format_field_value_user_field_server(
self, fields_mixin: FieldsMixin, mock_fields
):
"""Test format_field_value formats user fields correctly for Server/DC."""
# Set up the mocks
fields_mixin.get_field_by_id = MagicMock(
return_value=mock_fields[3]
) # The Assignee field
fields_mixin._get_account_id = MagicMock(return_value="jdoe")
fields_mixin.config = MagicMock()
fields_mixin.config.is_cloud = False
# Call the method with a user field and string value
result = fields_mixin.format_field_value("assignee", "johndoe")
# Verify the result — Server/DC uses name
assert result == {"name": "jdoe"}
fields_mixin._get_account_id.assert_called_once_with("johndoe")
def test_format_field_value_array_field(self, fields_mixin: FieldsMixin):
"""Test format_field_value formats array fields correctly."""
# Set up the mocks
mock_array_field = {
"id": "labels",
"name": "Labels",
"schema": {"type": "array"},
}
fields_mixin.get_field_by_id = MagicMock(return_value=mock_array_field)
# Test with single value (should convert to list)
result = fields_mixin.format_field_value("labels", "bug")
assert result == ["bug"]
# Test with list value (should keep as list)
result = fields_mixin.format_field_value("labels", ["bug", "feature"])
assert result == ["bug", "feature"]
def test_format_field_value_priority_field(self, fields_mixin: FieldsMixin):
"""Test format_field_value formats priority correctly with {name: ...}."""
mock_priority_field = {
"id": "priority",
"name": "Priority",
"schema": {"type": "priority"},
}
fields_mixin.get_field_by_id = MagicMock(return_value=mock_priority_field)
result = fields_mixin.format_field_value("priority", "High")
assert result == {"name": "High"}
already_formatted = {"name": "Medium"}
result = fields_mixin.format_field_value("priority", already_formatted)
assert result == already_formatted
def test_format_field_value_option_field(self, fields_mixin: FieldsMixin):
"""Test format_field_value formats option fields with {value: ...}."""
mock_option_field = {
"id": "customfield_10024",
"name": "Severity",
"schema": {"type": "option", "custom": "radiobuttons"},
}
fields_mixin.get_field_by_id = MagicMock(return_value=mock_option_field)
result = fields_mixin.format_field_value("customfield_10024", "Critical")
assert result == {"value": "Critical"}
already_formatted = {"value": "Medium"}
result = fields_mixin.format_field_value("customfield_10024", already_formatted)
assert result == already_formatted
def test_format_field_value_unknown_field(self, fields_mixin: FieldsMixin):
"""Test format_field_value returns value as-is for unknown fields."""
# Set up the mocks
fields_mixin.get_field_by_id = MagicMock(return_value=None)
# Call the method with unknown field
test_value = "test value"
result = fields_mixin.format_field_value("unknown", test_value)
# Verify the value is returned as-is
assert result == test_value
def test_search_fields_empty_keyword(self, fields_mixin: FieldsMixin, mock_fields):
"""Test search_fields returns first N fields when keyword is empty."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Call with empty keyword and limit=3
result = fields_mixin.search_fields("", limit=3)
# Verify first 3 fields are returned
assert len(result) == 3
assert result == mock_fields[:3]
def test_search_fields_exact_match(self, fields_mixin: FieldsMixin, mock_fields):
"""Test search_fields finds exact matches with high relevance."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Search for "Story Points"
result = fields_mixin.search_fields("Story Points")
# Verify Story Points field is first result
assert len(result) > 0
assert result[0]["name"] == "Story Points"
assert result[0]["id"] == "customfield_10012"
def test_search_fields_partial_match(self, fields_mixin: FieldsMixin, mock_fields):
"""Test search_fields finds partial matches."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Search for "Epic"
result = fields_mixin.search_fields("Epic")
# Verify Epic-related fields are in results
epic_fields = [field["name"] for field in result[:2]] # Top 2 results
assert "Epic Link" in epic_fields
assert "Epic Name" in epic_fields
def test_search_fields_case_insensitive(
self, fields_mixin: FieldsMixin, mock_fields
):
"""Test search_fields is case insensitive."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Search with different cases
result_lower = fields_mixin.search_fields("story points")
result_upper = fields_mixin.search_fields("STORY POINTS")
result_mixed = fields_mixin.search_fields("Story Points")
# Verify all searches find the same field
assert len(result_lower) > 0
assert len(result_upper) > 0
assert len(result_mixed) > 0
assert result_lower[0]["id"] == result_upper[0]["id"] == result_mixed[0]["id"]
assert result_lower[0]["name"] == "Story Points"
def test_search_fields_with_limit(self, fields_mixin: FieldsMixin, mock_fields):
"""Test search_fields respects the limit parameter."""
# Set up the fields
fields_mixin.get_fields = MagicMock(return_value=mock_fields)
# Search with limit=2
result = fields_mixin.search_fields("field", limit=2)
# Verify only 2 results are returned
assert len(result) == 2
def test_search_fields_error(self, fields_mixin: FieldsMixin):
"""Test search_fields handles errors gracefully."""
# Make get_fields raise an exception
fields_mixin.get_fields = MagicMock(
side_effect=Exception("Error getting fields")
)
# Call the method
result = fields_mixin.search_fields("test")
# Verify empty list is returned on error
assert result == []
class TestFormatFieldValueForWrite:
"""Tests for _format_field_value_for_write on FieldsMixin."""
@pytest.fixture
def mixin(self, jira_fetcher: JiraFetcher) -> FieldsMixin:
"""Create a FieldsMixin instance with mocked dependencies."""
fetcher = jira_fetcher
fetcher._get_account_id = MagicMock(return_value="resolved-id")
fetcher.config = MagicMock()
fetcher.config.is_cloud = True
return fetcher
# -- Priority --------------------------------------------------------
@pytest.mark.parametrize(
"test_id, field_id, value, field_definition, expected",
[
(
"priority_string",
"priority",
"High",
{"name": "Priority", "schema": {"type": "priority"}},
{"name": "High"},
),
(
"priority_dict",
"priority",
{"name": "High"},
{"name": "Priority", "schema": {"type": "priority"}},
{"name": "High"},
),
],
)
def test_priority(
self, mixin, test_id, field_id, value, field_definition, expected
):
result = mixin._format_field_value_for_write(field_id, value, field_definition)
assert result == expected
# -- Labels ----------------------------------------------------------
@pytest.mark.parametrize(
"test_id, field_id, value, field_definition, expected",
[
(
"labels_list",
"labels",
["a", "b"],
{"name": "Labels", "schema": {"type": "array", "items": "string"}},
["a", "b"],
),
(
"labels_csv",
"labels",
"a,b",
{"name": "Labels", "schema": {"type": "array", "items": "string"}},
["a", "b"],
),
],
)
def test_labels(self, mixin, test_id, field_id, value, field_definition, expected):
result = mixin._format_field_value_for_write(field_id, value, field_definition)
assert result == expected
# -- Components / fixVersions ----------------------------------------
@pytest.mark.parametrize(
"test_id, field_id, value, field_definition, expected",
[
(
"components_strings",
"components",
["UI", "API"],
{
"name": "Component/s",
"schema": {"type": "array", "items": "component"},
},
[{"name": "UI"}, {"name": "API"}],
),
(
"components_dicts",
"components",
[{"name": "UI"}],
{
"name": "Component/s",
"schema": {"type": "array", "items": "component"},
},
[{"name": "UI"}],
),
(
"fixversions",
"fixVersions",
["1.0"],
{
"name": "Fix Version/s",
"schema": {"type": "array", "items": "version"},
},
[{"name": "1.0"}],
),
],
)
def test_name_wrapped_arrays(
self, mixin, test_id, field_id, value, field_definition, expected
):
result = mixin._format_field_value_for_write(field_id, value, field_definition)
assert result == expected
# -- Reporter (Cloud vs Server) --------------------------------------
def test_reporter_cloud(self, mixin):
mixin.config.is_cloud = True
mixin._get_account_id = MagicMock(return_value="cloud-acc-id")
result = mixin._format_field_value_for_write(
"reporter",
"user@ex.com",
{"name": "Reporter", "schema": {"type": "user"}},
)
assert result == {"accountId": "cloud-acc-id"}
mixin._get_account_id.assert_called_once_with("user@ex.com")
def test_reporter_server(self, mixin):
mixin.config.is_cloud = False
mixin._get_account_id = MagicMock(return_value="jdoe")
result = mixin._format_field_value_for_write(
"reporter",
"jdoe",
{"name": "Reporter", "schema": {"type": "user"}},
)
assert result == {"name": "jdoe"}
mixin._get_account_id.assert_called_once_with("jdoe")
# -- Duedate ---------------------------------------------------------
def test_duedate_valid(self, mixin):
result = mixin._format_field_value_for_write("duedate", "2026-03-01", None)
assert result == "2026-03-01"
def test_duedate_invalid(self, mixin):
result = mixin._format_field_value_for_write("duedate", 12345, None)
assert result is None
# -- Cascading select ------------------------------------------------
@pytest.mark.parametrize(
"test_id, value, expected",
[
(
"cascading_tuple",
("NA", "US"),
{"value": "NA", "child": {"value": "US"}},
),
(
"cascading_dict",
{"value": "NA", "child": {"value": "US"}},
{"value": "NA", "child": {"value": "US"}},
),
(
"cascading_string",
"NA",
{"value": "NA"},
),
],
)
def test_cascading_select(self, mixin, test_id, value, expected):
field_def = {
"name": "Region",
"schema": {"type": "option-with-child", "custom": "cascadingselect"},
}
result = mixin._format_field_value_for_write(
"customfield_10020", value, field_def
)
assert result == expected
# -- Multi-select ----------------------------------------------------
@pytest.mark.parametrize(
"test_id, value, expected",
[
(
"multiselect_strings",
["opt1", "opt2"],
[{"value": "opt1"}, {"value": "opt2"}],
),
(
"multiselect_dicts",
[{"value": "opt1"}],
[{"value": "opt1"}],
),
(
"multiselect_csv",
"opt1,opt2",
[{"value": "opt1"}, {"value": "opt2"}],
),
],
)
def test_multiselect(self, mixin, test_id, value, expected):
field_def = {
"name": "Categories",
"schema": {"type": "array", "items": "option", "custom": "multiselect"},
}
result = mixin._format_field_value_for_write(
"customfield_10021", value, field_def
)
assert result == expected
# -- Custom user field -----------------------------------------------
def test_custom_user_cloud(self, mixin):
mixin.config.is_cloud = True
mixin._get_account_id = MagicMock(return_value="cloud-acc-id")
field_def = {
"name": "Reviewer",
"schema": {"type": "user", "custom": "userpicker"},
}
result = mixin._format_field_value_for_write(
"customfield_10022", "user@ex.com", field_def
)
assert result == {"accountId": "cloud-acc-id"}
def test_custom_user_server(self, mixin):
mixin.config.is_cloud = False
mixin._get_account_id = MagicMock(return_value="jdoe")
field_def = {
"name": "Reviewer",
"schema": {"type": "user", "custom": "userpicker"},
}
result = mixin._format_field_value_for_write(
"customfield_10022", "jdoe", field_def
)
assert result == {"name": "jdoe"}
def test_custom_user_unresolvable(self, mixin):
"""Unresolvable custom user field returns None instead of raising."""
mixin._get_account_id = MagicMock(side_effect=ValueError("User not found"))
field_def = {
"name": "Reviewer",
"schema": {"type": "user", "custom": "userpicker"},
}
result = mixin._format_field_value_for_write(
"customfield_10022", "nobody@ex.com", field_def
)
assert result is None
# -- Custom date field -----------------------------------------------
def test_custom_date_valid(self, mixin):
field_def = {"name": "Target Date", "schema": {"type": "date"}}
result = mixin._format_field_value_for_write(
"customfield_10023", "2026-03-01", field_def
)
assert result == "2026-03-01"
def test_custom_date_invalid(self, mixin):
field_def = {"name": "Target Date", "schema": {"type": "date"}}
result = mixin._format_field_value_for_write(
"customfield_10023", 12345, field_def
)
assert result is None
# -- Generic option (radio/select) -----------------------------------
@pytest.mark.parametrize(
"test_id, value, expected",
[
("option_string", "Critical", {"value": "Critical"}),
("option_dict", {"value": "Critical"}, {"value": "Critical"}),
],
)
def test_option_field(self, mixin, test_id, value, expected):
field_def = {
"name": "Severity",
"schema": {"type": "option", "custom": "radiobuttons"},
}
result = mixin._format_field_value_for_write(
"customfield_10024", value, field_def
)
assert result == expected
# -- Unknown field passthrough ---------------------------------------
def test_unknown_passthrough(self, mixin):
result = mixin._format_field_value_for_write(
"customfield_99999", "anything", None
)
assert result == "anything"
class TestDatetimeTimezoneFormat:
"""Test datetime field formatting produces Jira-compatible tz offsets."""
@pytest.fixture
def fields_mixin(self, jira_fetcher: "JiraFetcher") -> FieldsMixin:
"""Create a FieldsMixin instance with mocked dependencies."""
mixin = jira_fetcher
mixin._field_ids_cache = None
return mixin
@pytest.mark.parametrize(
"input_value,expected",
[
pytest.param(
"2026-01-21T15:00:00.000+0000",
"2026-01-21T15:00:00.000+0000",
id="already-basic-format",
),
pytest.param(
"2026-01-21T15:00:00+00:00",
"2026-01-21T15:00:00.000+0000",
id="extended-to-basic",
),
pytest.param(
"2026-01-21T15:00:00.000+0530",
"2026-01-21T15:00:00.000+0530",
id="non-utc-preserved",
),
pytest.param(
"2026-01-21T15:00:00-0800",
"2026-01-21T15:00:00.000-0800",
id="negative-offset-basic",
),
pytest.param(
"2026-01-21T15:00:00",
"2026-01-21T15:00:00.000",
id="naive-no-tz",
),
pytest.param(
"2026-01-21",
"2026-01-21T00:00:00.000",
id="date-only-to-midnight",
),
pytest.param(
"invalid-date",
"invalid-date",
id="unparseable-passthrough",
),
pytest.param(
"",
"",
id="empty-string",
),
],
)
def test_datetime_timezone_format(
self, fields_mixin: FieldsMixin, input_value: str, expected: str
):
"""Datetime fields must use ±HHMM (basic) format, not ±HH:MM."""
mock_datetime_field = {
"id": "customfield_10050",
"name": "Due DateTime",
"schema": {"type": "datetime"},
}
fields_mixin.get_field_by_id = MagicMock(return_value=mock_datetime_field)
result = fields_mixin.format_field_value("customfield_10050", input_value)
assert result == expected
class TestChecklistFieldFormatting:
"""Tests for checklist field formatting in _format_field_value_for_write."""
CHECKLIST_FIELD_DEF = {
"id": "customfield_11003",
"name": "Definition of Done",
"schema": {
"type": "string",
"custom": "com.okapya.jira.checklist:checklist",
"customId": 11003,
},
}
CHECKLIST_ARRAY_FIELD_DEF = {
"id": "customfield_11004",
"name": "Checklist",
"schema": {
"type": "array",
"items": "checklist-item",
"custom": "com.okapya.jira.checklist:checklist",
"customId": 11004,
},
}
@pytest.fixture
def mixin(self, jira_fetcher: "JiraFetcher") -> FieldsMixin:
"""Create a FieldsMixin instance with mocked dependencies."""
fetcher = jira_fetcher
fetcher.config = MagicMock()
fetcher.config.is_cloud = True
return fetcher
@pytest.mark.parametrize(
"test_id, value, expected",
[
pytest.param(
"list_to_markdown",
["Task A", "Task B"],
"* Task A\n* Task B",
id="list_to_markdown",
),
pytest.param(
"list_with_checked_tuples",
[("Task A", True), ("Task B", False)],
"* [x] Task A\n* Task B",
id="list_with_checked_tuples",
),
pytest.param(
"dict_list",
[{"name": "Task A", "checked": True}],
"* [x] Task A",
id="dict_list",
),
pytest.param(
"string_passthrough",
"* [x] done\n* todo",
"* [x] done\n* todo",
id="string_passthrough",
),
pytest.param(
"empty_list",
[],
"",
id="empty_list",
),
],
)
def test_checklist_formatting(self, mixin, test_id, value, expected):
"""Checklist fields should be converted to markdown string format."""
result = mixin._format_field_value_for_write(
"customfield_11003", value, self.CHECKLIST_FIELD_DEF
)
assert result == expected
@pytest.mark.parametrize(
"test_id, value, expected",
[
pytest.param(
"array_dict_list_passthrough",
[{"name": "A", "checked": True}, {"name": "B", "checked": False}],
[{"name": "A", "checked": True}, {"name": "B", "checked": False}],
id="array_dict_list_passthrough",
),
pytest.param(
"array_string_list_passthrough",
["Task A", "Task B"],
["Task A", "Task B"],
id="array_string_list_passthrough",
),
pytest.param(
"array_string_passthrough",
"* [x] done\n* todo",
"* [x] done\n* todo",
id="array_string_passthrough",
),
pytest.param(
"array_empty_list_passthrough",
[],
[],
id="array_empty_list_passthrough",
),
],
)
def test_checklist_array_passthrough(self, mixin, test_id, value, expected):
"""Array-type checklist fields (Server/DC) should pass through unchanged."""
result = mixin._format_field_value_for_write(
"customfield_11004", value, self.CHECKLIST_ARRAY_FIELD_DEF
)
assert result == expected
def test_non_checklist_string_field_unaffected(self, mixin):
"""Non-checklist string fields should not be affected by checklist logic."""
non_checklist_def = {
"id": "customfield_99999",
"name": "Some Text Field",
"schema": {"type": "string"},
}
value = ["a", "b"]
result = mixin._format_field_value_for_write(
"customfield_99999", value, non_checklist_def
)
# Should pass through unchanged (no checklist conversion)
assert result == ["a", "b"]