fix: resolve --user-login by exact match
Redmine user.filter(login=...) performs an inexact substring search, but _resolve_user_id took users[0].id unconditionally, so a report could silently be built for the wrong user (e.g. 'ivanov' matching 'ivanov2'). Now only exact (case-sensitive) login matches are considered: exactly one match resolves to its id, multiple matches raise an ambiguity error, no match falls through to the name lookup and then to a 'not found' error — symmetric with the --user-name resolution. Closes #60
This commit is contained in:
@@ -275,6 +275,7 @@ def test_fetch_resolves_user_login_to_id(mock_redmine_class):
|
||||
|
||||
mock_user = mock.MagicMock()
|
||||
mock_user.id = 7
|
||||
mock_user.login = "ivanov"
|
||||
mock_redmine.user.filter.return_value = [mock_user]
|
||||
mock_redmine.time_entry.filter.return_value = []
|
||||
|
||||
@@ -287,6 +288,91 @@ def test_fetch_resolves_user_login_to_id(mock_redmine_class):
|
||||
assert kwargs["user_id"] == 7
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_rejects_inexact_login_match(mock_redmine_class):
|
||||
"""Фильтр Redmine по логину неточный (#60): без точного совпадения — ошибка."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
near_match = mock.MagicMock()
|
||||
near_match.id = 8
|
||||
near_match.login = "ivanov2"
|
||||
mock_redmine.user.filter.side_effect = [
|
||||
[near_match], # неточное совпадение по логину
|
||||
[], # по имени никого не найдено
|
||||
]
|
||||
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
with pytest.raises(RedmineAPIError, match="not found"):
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31", user_id="ivanov")
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_resolves_exact_login_among_near_matches(mock_redmine_class):
|
||||
"""Среди неточных результатов фильтра выбирается точное совпадение логина (#60)."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
near = mock.MagicMock()
|
||||
near.id = 8
|
||||
near.login = "ivanov2"
|
||||
exact = mock.MagicMock()
|
||||
exact.id = 7
|
||||
exact.login = "ivanov"
|
||||
mock_redmine.user.filter.return_value = [near, exact]
|
||||
mock_redmine.time_entry.filter.return_value = []
|
||||
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31", user_id="ivanov")
|
||||
|
||||
_, kwargs = mock_redmine.time_entry.filter.call_args
|
||||
assert kwargs["user_id"] == 7
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_rejects_multiple_exact_login_matches(mock_redmine_class):
|
||||
"""Несколько пользователей с точным логином — ошибка неоднозначности (#60)."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
user1 = mock.MagicMock()
|
||||
user1.id = 7
|
||||
user1.login = "ivanov"
|
||||
user2 = mock.MagicMock()
|
||||
user2.id = 9
|
||||
user2.login = "ivanov"
|
||||
mock_redmine.user.filter.return_value = [user1, user2]
|
||||
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
with pytest.raises(RedmineAPIError, match="Multiple users match"):
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31", user_id="ivanov")
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_login_exact_match_is_case_sensitive(mock_redmine_class):
|
||||
"""Точное совпадение логина регистрозависимое (#60)."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
other_case = mock.MagicMock()
|
||||
other_case.id = 8
|
||||
other_case.login = "Ivanov"
|
||||
mock_redmine.user.filter.side_effect = [
|
||||
[other_case], # логин отличается регистром
|
||||
[], # по имени никого не найдено
|
||||
]
|
||||
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
with pytest.raises(RedmineAPIError, match="not found"):
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31", user_id="ivanov")
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_resolves_user_name_to_id(mock_redmine_class):
|
||||
|
||||
Reference in New Issue
Block a user