Skip to content

Commit 0f7736e

Browse files
authored
fix: prioritize user name over key for Jira Server/DC assignee field (#298)
Fixes a bug where setting assignees in Jira Server/Data Center environments would fail due to providing the user's key instead of name in the assignee field. Modified lookup functions to prioritize returning the user's name for non-cloud environments to ensure compatibility with Jira Server/DC API that expects the 'name' value in the assignee.name field.
1 parent 2fe1235 commit 0f7736e

3 files changed

Lines changed: 133 additions & 47 deletions

File tree

src/mcp_atlassian/jira/issues.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -540,8 +540,9 @@ def create_issue(
540540
# Add assignee if provided
541541
if assignee:
542542
try:
543-
account_id = self._get_account_id(assignee)
544-
self._add_assignee_to_fields(fields, account_id)
543+
# _get_account_id now returns the correct identifier (accountId for cloud, name for server)
544+
assignee_identifier = self._get_account_id(assignee)
545+
self._add_assignee_to_fields(fields, assignee_identifier)
545546
except ValueError as e:
546547
logger.warning(f"Could not assign issue: {str(e)}")
547548

@@ -1287,8 +1288,9 @@ def batch_create_issues(
12871288
# Add assignee if provided
12881289
if assignee:
12891290
try:
1290-
account_id = self._get_account_id(assignee)
1291-
self._add_assignee_to_fields(fields, account_id)
1291+
# _get_account_id now returns the correct identifier (accountId for cloud, name for server)
1292+
assignee_identifier = self._get_account_id(assignee)
1293+
self._add_assignee_to_fields(fields, assignee_identifier)
12921294
except ValueError as e:
12931295
logger.warning(f"Could not assign issue: {str(e)}")
12941296

src/mcp_atlassian/jira/users.py

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,25 @@ def _lookup_user_directly(self, username: str) -> str | None:
140140
or user.get("name", "").lower() == username.lower()
141141
or user.get("emailAddress", "").lower() == username.lower()
142142
):
143-
# Jira Cloud uses accountId
144-
if "accountId" in user:
145-
return user["accountId"]
146-
# Jira Data Center/Server might use key
147-
elif "key" in user:
148-
logger.info(
149-
"Using 'key' instead of 'accountId' for Jira Data Center/Server"
150-
)
151-
return user["key"]
152-
# Last resort fallback to name
153-
elif "name" in user:
154-
logger.info(
155-
"Using 'name' instead of 'accountId' for Jira Data Center/Server"
156-
)
157-
return user["name"]
143+
# Prioritize based on Cloud vs Server/DC for assignee field compatibility
144+
if self.config.is_cloud:
145+
# Cloud requires accountId
146+
if "accountId" in user:
147+
return user["accountId"]
148+
else:
149+
# Server/DC requires 'name' for the assignee field { "name": ... }
150+
if "name" in user:
151+
logger.info(
152+
"Using 'name' for assignee field in Jira Data Center/Server"
153+
)
154+
return user["name"]
155+
# Fallback to key if name is somehow missing (less common)
156+
elif "key" in user:
157+
logger.info(
158+
"Using 'key' as fallback for assignee name in Jira Data Center/Server"
159+
)
160+
return user["key"]
161+
158162
return None
159163
except Exception as e:
160164
logger.info(f"Error looking up user directly: {str(e)}")
@@ -195,21 +199,24 @@ def _lookup_user_by_permissions(self, username: str) -> str | None:
195199
if response.status_code == 200:
196200
data = response.json()
197201
for user in data.get("users", []):
198-
# Jira Cloud uses accountId
199-
if "accountId" in user:
200-
return user["accountId"]
201-
# Jira Data Center/Server might use key
202-
elif "key" in user:
203-
logger.info(
204-
"Using 'key' instead of 'accountId' for Jira Data Center/Server"
205-
)
206-
return user["key"]
207-
# Last resort fallback to name
208-
elif "name" in user:
209-
logger.info(
210-
"Using 'name' instead of 'accountId' for Jira Data Center/Server"
211-
)
212-
return user["name"]
202+
# Prioritize based on Cloud vs Server/DC for assignee field compatibility
203+
if self.config.is_cloud:
204+
# Cloud requires accountId
205+
if "accountId" in user:
206+
return user["accountId"]
207+
else:
208+
# Server/DC requires 'name' for the assignee field { "name": ... }
209+
if "name" in user:
210+
logger.info(
211+
"Using 'name' for assignee field in Jira Data Center/Server"
212+
)
213+
return user["name"]
214+
# Fallback to key if name is somehow missing (less common)
215+
elif "key" in user:
216+
logger.info(
217+
"Using 'key' as fallback for assignee name in Jira Data Center/Server"
218+
)
219+
return user["key"]
213220
return None
214221
except Exception as e:
215222
logger.info(f"Error looking up user by permissions: {str(e)}")

tests/unit/jira/test_users.py

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def test_lookup_user_directly_server_dc(self, users_mixin):
256256
users_mixin.jira.user_find_by_user_string.return_value = [
257257
{
258258
"key": "server-user-key",
259+
"name": "server-user-name",
259260
"displayName": "Test User",
260261
"emailAddress": "test@example.com",
261262
}
@@ -268,7 +269,32 @@ def test_lookup_user_directly_server_dc(self, users_mixin):
268269
# Call the method
269270
account_id = users_mixin._lookup_user_directly("Test User")
270271

271-
# Verify result
272+
# Verify result - should now return name instead of key for Server/DC
273+
assert account_id == "server-user-name"
274+
# Verify API call with username parameter for Server/DC
275+
users_mixin.jira.user_find_by_user_string.assert_called_once_with(
276+
username="Test User", start=0, limit=1
277+
)
278+
279+
def test_lookup_user_directly_server_dc_key_fallback(self, users_mixin):
280+
"""Test _lookup_user_directly for Server/DC falls back to key when name is not available."""
281+
# Mock the API response
282+
users_mixin.jira.user_find_by_user_string.return_value = [
283+
{
284+
"key": "server-user-key", # Only key, no name
285+
"displayName": "Test User",
286+
"emailAddress": "test@example.com",
287+
}
288+
]
289+
290+
# Mock config.is_cloud to return False for Server/DC
291+
users_mixin.config = MagicMock()
292+
users_mixin.config.is_cloud = False
293+
294+
# Call the method
295+
account_id = users_mixin._lookup_user_directly("Test User")
296+
297+
# Verify result - should fallback to key when name is missing
272298
assert account_id == "server-user-key"
273299
# Verify API call with username parameter for Server/DC
274300
users_mixin.jira.user_find_by_user_string.assert_called_once_with(
@@ -301,14 +327,18 @@ def test_lookup_user_directly_jira_data_center_key(self, users_mixin):
301327
}
302328
]
303329

330+
# Mock config.is_cloud to return False for Server/DC
331+
users_mixin.config = MagicMock()
332+
users_mixin.config.is_cloud = False
333+
304334
# Call the method
305335
account_id = users_mixin._lookup_user_directly("Test User")
306336

307337
# Verify result
308338
assert account_id == "data-center-key"
309339
# Verify API call
310340
users_mixin.jira.user_find_by_user_string.assert_called_once_with(
311-
query="Test User", start=0, limit=1
341+
username="Test User", start=0, limit=1
312342
)
313343

314344
def test_lookup_user_directly_jira_data_center_name(self, users_mixin):
@@ -322,14 +352,18 @@ def test_lookup_user_directly_jira_data_center_name(self, users_mixin):
322352
}
323353
]
324354

355+
# Mock config.is_cloud to return False for Server/DC
356+
users_mixin.config = MagicMock()
357+
users_mixin.config.is_cloud = False
358+
325359
# Call the method
326360
account_id = users_mixin._lookup_user_directly("Test User")
327361

328362
# Verify result
329363
assert account_id == "data-center-name"
330364
# Verify API call
331365
users_mixin.jira.user_find_by_user_string.assert_called_once_with(
332-
query="Test User", start=0, limit=1
366+
username="Test User", start=0, limit=1
333367
)
334368

335369
def test_lookup_user_directly_error(self, users_mixin):
@@ -382,22 +416,31 @@ def test_lookup_user_by_permissions_not_found(self, users_mixin):
382416
# Verify result
383417
assert account_id is None
384418

385-
def test_lookup_user_by_permissions_jira_data_center_key(self, users_mixin):
386-
"""Test _lookup_user_by_permissions when only 'key' is available (Data Center)."""
419+
def test_lookup_user_by_permissions_jira_data_center(self, users_mixin):
420+
"""Test _lookup_user_by_permissions when both 'key' and 'name' are available (Data Center)."""
387421
# Mock requests.get
388422
with patch("requests.get") as mock_get:
389423
mock_response = MagicMock()
390424
mock_response.status_code = 200
391425
mock_response.json.return_value = {
392-
"users": [{"key": "data-center-permissions-key"}]
426+
"users": [
427+
{
428+
"key": "data-center-permissions-key",
429+
"name": "data-center-permissions-name",
430+
}
431+
]
393432
}
394433
mock_get.return_value = mock_response
395434

435+
# Mock config.is_cloud to return False for Server/DC
436+
users_mixin.config = MagicMock()
437+
users_mixin.config.is_cloud = False
438+
396439
# Call the method
397440
account_id = users_mixin._lookup_user_by_permissions("username")
398441

399-
# Verify result
400-
assert account_id == "data-center-permissions-key"
442+
# Verify result - should prioritize name for Server/DC
443+
assert account_id == "data-center-permissions-name"
401444
# Verify API call
402445
mock_get.assert_called_once()
403446
assert mock_get.call_args[0][0].endswith("/user/permission/search")
@@ -406,22 +449,28 @@ def test_lookup_user_by_permissions_jira_data_center_key(self, users_mixin):
406449
"permissions": "BROWSE",
407450
}
408451

409-
def test_lookup_user_by_permissions_jira_data_center_name(self, users_mixin):
410-
"""Test _lookup_user_by_permissions when only 'name' is available (Data Center)."""
452+
def test_lookup_user_by_permissions_jira_data_center_key_fallback(
453+
self, users_mixin
454+
):
455+
"""Test _lookup_user_by_permissions when only 'key' is available (Data Center)."""
411456
# Mock requests.get
412457
with patch("requests.get") as mock_get:
413458
mock_response = MagicMock()
414459
mock_response.status_code = 200
415460
mock_response.json.return_value = {
416-
"users": [{"name": "data-center-permissions-name"}]
461+
"users": [{"key": "data-center-permissions-key"}]
417462
}
418463
mock_get.return_value = mock_response
419464

465+
# Mock config.is_cloud to return False for Server/DC
466+
users_mixin.config = MagicMock()
467+
users_mixin.config.is_cloud = False
468+
420469
# Call the method
421470
account_id = users_mixin._lookup_user_by_permissions("username")
422471

423-
# Verify result
424-
assert account_id == "data-center-permissions-name"
472+
# Verify result - should fallback to key when name is missing
473+
assert account_id == "data-center-permissions-key"
425474
# Verify API call
426475
mock_get.assert_called_once()
427476
assert mock_get.call_args[0][0].endswith("/user/permission/search")
@@ -439,3 +488,31 @@ def test_lookup_user_by_permissions_error(self, users_mixin):
439488

440489
# Verify result
441490
assert account_id is None
491+
492+
def test_lookup_user_by_permissions_jira_data_center_name_only(self, users_mixin):
493+
"""Test _lookup_user_by_permissions when only 'name' is available (Data Center)."""
494+
# Mock requests.get
495+
with patch("requests.get") as mock_get:
496+
mock_response = MagicMock()
497+
mock_response.status_code = 200
498+
mock_response.json.return_value = {
499+
"users": [{"name": "data-center-permissions-name"}]
500+
}
501+
mock_get.return_value = mock_response
502+
503+
# Mock config.is_cloud to return False for Server/DC
504+
users_mixin.config = MagicMock()
505+
users_mixin.config.is_cloud = False
506+
507+
# Call the method
508+
account_id = users_mixin._lookup_user_by_permissions("username")
509+
510+
# Verify result - should use name when that's all that's available
511+
assert account_id == "data-center-permissions-name"
512+
# Verify API call
513+
mock_get.assert_called_once()
514+
assert mock_get.call_args[0][0].endswith("/user/permission/search")
515+
assert mock_get.call_args[1]["params"] == {
516+
"query": "username",
517+
"permissions": "BROWSE",
518+
}

0 commit comments

Comments
 (0)