Skip to content

Commit 9ded217

Browse files
authored
On Jira Server/DC, GET /rest/api/2/user?username= matches only the login (#998)
name, not email address. When an email identifier is given, _determine_user_api_params now calls _lookup_user_directly first (GET /rest/api/2/user/search?username=), which searches by email, display name, and login, and then uses the resolved username or key for the actual profile fetch. A fallback to passing the email directly as username is kept for cases where the login name is the email itself. Regression tests for the Server/DC email flow are added.
1 parent a85b1ef commit 9ded217

2 files changed

Lines changed: 86 additions & 4 deletions

File tree

src/mcp_atlassian/jira/users.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,10 +259,27 @@ def _determine_user_api_params(self, identifier: str) -> dict[str, str]:
259259
# Server/DC: username, key, or email
260260
elif not self.config.is_cloud:
261261
if "@" in identifier:
262-
api_kwargs["username"] = identifier
263-
logger.debug(
264-
f"Determined param: username='{identifier}' (Server/DC email - might not work)"
265-
)
262+
# /rest/api/2/user?username=email won't match by email on Server/DC.
263+
# Use /rest/api/2/user/search first to resolve email → actual username/key.
264+
resolved = self._lookup_user_directly(identifier)
265+
if resolved:
266+
if "-" in resolved and any(c.isdigit() for c in resolved):
267+
api_kwargs["key"] = resolved
268+
logger.debug(
269+
f"Resolved email '{identifier}' to key '{resolved}' (Server/DC)"
270+
)
271+
else:
272+
api_kwargs["username"] = resolved
273+
logger.debug(
274+
f"Resolved email '{identifier}' to username '{resolved}' (Server/DC)"
275+
)
276+
else:
277+
# Fallback: try email as username directly (works if login name IS the email)
278+
api_kwargs["username"] = identifier
279+
logger.debug(
280+
f"Could not resolve email '{identifier}' via search, "
281+
f"trying as username directly (Server/DC)"
282+
)
266283
elif "-" in identifier and any(c.isdigit() for c in identifier):
267284
api_kwargs["key"] = identifier
268285
logger.debug(f"Determined param: key='{identifier}' (Server/DC)")

tests/unit/jira/test_users.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,71 @@ def test_lookup_user_by_permissions_jira_data_center_name_only(self, users_mixin
504504
"permissions": "BROWSE",
505505
}
506506

507+
def test_determine_user_api_params_server_dc_email_resolved_to_username(
508+
self, users_mixin
509+
):
510+
"""Test Server/DC email is resolved via search, not passed directly as username."""
511+
users_mixin.config = MagicMock(spec=JiraConfig)
512+
users_mixin.config.is_cloud = False
513+
users_mixin._lookup_user_directly = MagicMock(return_value="jnovak")
514+
515+
params = users_mixin._determine_user_api_params("jnovak@firma.cz")
516+
517+
assert params == {"username": "jnovak"}
518+
users_mixin._lookup_user_directly.assert_called_once_with("jnovak@firma.cz")
519+
520+
def test_determine_user_api_params_server_dc_email_resolved_to_key(
521+
self, users_mixin
522+
):
523+
"""Test Server/DC email resolving to a key-style identifier."""
524+
users_mixin.config = MagicMock(spec=JiraConfig)
525+
users_mixin.config.is_cloud = False
526+
users_mixin._lookup_user_directly = MagicMock(return_value="JIRAUSER-12345")
527+
528+
params = users_mixin._determine_user_api_params("jnovak@firma.cz")
529+
530+
assert params == {"key": "JIRAUSER-12345"}
531+
532+
def test_determine_user_api_params_server_dc_email_lookup_fails_fallback(
533+
self, users_mixin
534+
):
535+
"""Test Server/DC email falls back to direct username when lookup returns None."""
536+
users_mixin.config = MagicMock(spec=JiraConfig)
537+
users_mixin.config.is_cloud = False
538+
users_mixin._lookup_user_directly = MagicMock(return_value=None)
539+
540+
params = users_mixin._determine_user_api_params("login@example.com")
541+
542+
# Fallback: email used as username directly (e.g., when login IS the email)
543+
assert params == {"username": "login@example.com"}
544+
545+
def test_get_user_profile_by_identifier_server_dc_email(self, users_mixin):
546+
"""Regression: Server/DC email lookup must search first, not pass email as username."""
547+
users_mixin.config = MagicMock(spec=JiraConfig)
548+
users_mixin.config.is_cloud = False
549+
users_mixin._lookup_user_directly = MagicMock(return_value="jnovak")
550+
551+
with patch(
552+
"src.mcp_atlassian.jira.users.JiraUser.from_api_response"
553+
) as mock_from_api_response:
554+
mock_user_instance = MagicMock()
555+
mock_from_api_response.return_value = mock_user_instance
556+
mock_response_data = {
557+
"name": "jnovak",
558+
"displayName": "Jan Novák",
559+
"emailAddress": "jnovak@firma.cz",
560+
"active": True,
561+
}
562+
users_mixin.jira.user = MagicMock(return_value=mock_response_data)
563+
564+
user = users_mixin.get_user_profile_by_identifier("jnovak@firma.cz")
565+
566+
assert user == mock_user_instance
567+
# Must resolve email to username first
568+
users_mixin._lookup_user_directly.assert_called_once_with("jnovak@firma.cz")
569+
# Must call user() with resolved username, NOT the raw email
570+
users_mixin.jira.user.assert_called_once_with(username="jnovak")
571+
507572
def test_get_user_profile_by_identifier_cloud_account_id(self, users_mixin):
508573
"""Test get_user_profile_by_identifier with Cloud and accountId."""
509574
users_mixin.config = MagicMock(spec=JiraConfig)

0 commit comments

Comments
 (0)