feat(user): report on another user's time entries
- Add user_id parameter to fetch_issues_with_spent_time(). - Support numeric ID, login, or full name resolution. - Reject ambiguous names and unknown users with clear messages. - Add CLI flags: --user-id, --user-login, --user-name. - Only allow one user flag at a time. - Add ResourceNotFoundError handling. - Update README with usage examples. - Add tests for user resolution and CLI flags. Closes #40
This commit is contained in:
@@ -265,3 +265,39 @@ def test_cli_no_time_passed_to_formatter(mock_fetch, tmp_path):
|
||||
|
||||
_, kwargs = mock_get_formatter.call_args
|
||||
assert kwargs.get("no_time") is True
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_cli_passes_user_id_to_fetch(mock_fetch):
|
||||
"""CLI --user-id передаётся в fetch_issues_with_spent_time."""
|
||||
mock_fetch.return_value = None
|
||||
main(["--date", "2026-01-01--2026-01-31", "--user-id", "42"])
|
||||
_, kwargs = mock_fetch.call_args
|
||||
assert kwargs["user_id"] == "42"
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_cli_passes_user_login_to_fetch(mock_fetch):
|
||||
"""CLI --user-login передаётся в fetch_issues_with_spent_time."""
|
||||
mock_fetch.return_value = None
|
||||
main(["--date", "2026-01-01--2026-01-31", "--user-login", "ivanov"])
|
||||
_, kwargs = mock_fetch.call_args
|
||||
assert kwargs["user_id"] == "ivanov"
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
def test_cli_rejects_multiple_user_flags():
|
||||
"""CLI не принимает одновременно несколько флагов пользователя."""
|
||||
code = main(
|
||||
[
|
||||
"--date",
|
||||
"2026-01-01--2026-01-31",
|
||||
"--user-id",
|
||||
"42",
|
||||
"--user-login",
|
||||
"ivanov",
|
||||
]
|
||||
)
|
||||
assert code == 1
|
||||
|
||||
@@ -249,6 +249,112 @@ def test_fetch_raises_redmine_api_error_on_http_500(mock_redmine_class):
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31")
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_uses_specified_user_id(mock_redmine_class):
|
||||
"""user_id передаётся в time_entry.filter вместо текущего пользователя."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
mock_redmine.time_entry.filter.return_value = []
|
||||
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31", user_id=42)
|
||||
|
||||
_, kwargs = mock_redmine.time_entry.filter.call_args
|
||||
assert kwargs["user_id"] == 42
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_resolves_user_login_to_id(mock_redmine_class):
|
||||
"""Строковый логин разрешается в числовой ID."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
mock_user = mock.MagicMock()
|
||||
mock_user.id = 7
|
||||
mock_redmine.user.filter.return_value = [mock_user]
|
||||
mock_redmine.time_entry.filter.return_value = []
|
||||
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31", user_id="ivanov")
|
||||
|
||||
login_filter_call = mock_redmine.user.filter.call_args_list[0]
|
||||
assert login_filter_call.kwargs == {"login": "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_resolves_user_name_to_id(mock_redmine_class):
|
||||
"""Строковое имя разрешается в числовой ID, если найден ровно один пользователь."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
mock_redmine.user.filter.side_effect = [
|
||||
[], # поиск по логину не дал результатов
|
||||
[mock.MagicMock(id=9)], # поиск по имени нашёл одного
|
||||
]
|
||||
mock_redmine.time_entry.filter.return_value = []
|
||||
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31", user_id="Иванов И.И.")
|
||||
|
||||
name_filter_call = mock_redmine.user.filter.call_args_list[1]
|
||||
assert name_filter_call.kwargs == {"name": "Иванов И.И."}
|
||||
|
||||
_, kwargs = mock_redmine.time_entry.filter.call_args
|
||||
assert kwargs["user_id"] == 9
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_rejects_ambiguous_user_name(mock_redmine_class):
|
||||
"""Если по имени найдено несколько пользователей — ошибка с понятным сообщением."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
mock_redmine.user.filter.side_effect = [
|
||||
[], # поиск по логину не дал результатов
|
||||
[mock.MagicMock(id=1), mock.MagicMock(id=2)], # по имени — несколько
|
||||
]
|
||||
|
||||
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="Иванов")
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_rejects_unknown_user_login(mock_redmine_class):
|
||||
"""Если пользователь не найден — ошибка с предложением использовать --user-id."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
|
||||
mock_redmine.user.filter.return_value = []
|
||||
|
||||
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="unknown")
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_numeric_string_user_id(mock_redmine_class):
|
||||
"""Строка из цифр интерпретируется как числовой ID."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine)
|
||||
mock_redmine.time_entry.filter.return_value = []
|
||||
|
||||
fetch_issues_with_spent_time("2026-01-01", "2026-01-31", user_id="42")
|
||||
|
||||
assert mock_redmine.user.filter.call_count == 0
|
||||
_, kwargs = mock_redmine.time_entry.filter.call_args
|
||||
assert kwargs["user_id"] == 42
|
||||
|
||||
|
||||
# -- #24: Таймаут и retry --
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user