Merge branch 'fix/55-cli-sanitize'
This commit is contained in:
@@ -1452,3 +1452,119 @@ def test_cli_dynamic_datetime_period_without_date(mock_fetch, tmp_path):
|
||||
# Следующий период после 2026-06-01T00:00:00--2026-06-30T23:59:59
|
||||
assert from_date == "2026-07-01T00:00:00"
|
||||
assert to_date == "2026-07-30T23:59:59"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #55: sanitize Redmine exception output in CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSanitizeErrorOutput:
|
||||
"""CLI не должен выводить API-ключ из текста исключений в stderr (#55)."""
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_api_key_in_url_is_masked(self, mock_fetch, capsys):
|
||||
"""key=SECRET в URL внутри сообщения исключения маскируется."""
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
mock_fetch.side_effect = RedmineAPIError(
|
||||
"Network error while calling Redmine: "
|
||||
"HTTPSConnectionPool(host='red.example.com', port=443): "
|
||||
"Failed https://red.example.com/issues.json?key=SECRET123&limit=100"
|
||||
)
|
||||
code = main(["--date", "2026-01-01--2026-01-31"])
|
||||
captured = capsys.readouterr()
|
||||
assert code == 1
|
||||
assert "SECRET123" not in captured.err
|
||||
assert "key=***" in captured.err
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_api_key_header_is_masked(self, mock_fetch, capsys):
|
||||
"""Заголовок X-Redmine-API-Key в тексте исключения маскируется."""
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
mock_fetch.side_effect = RedmineAPIError(
|
||||
"Request failed: headers {'X-Redmine-API-Key': 'SECRET123'}"
|
||||
)
|
||||
code = main(["--date", "2026-01-01--2026-01-31"])
|
||||
captured = capsys.readouterr()
|
||||
assert code == 1
|
||||
assert "SECRET123" not in captured.err
|
||||
assert "X-Redmine-API-Key" in captured.err
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_unexpected_exception_is_masked(self, mock_fetch, capsys):
|
||||
"""Общее исключение с key=SECRET в тексте тоже санитизируется."""
|
||||
mock_fetch.side_effect = RuntimeError(
|
||||
"boom while requesting https://red.example.com/t.json?key=SECRET123"
|
||||
)
|
||||
code = main(["--date", "2026-01-01--2026-01-31"])
|
||||
captured = capsys.readouterr()
|
||||
assert code == 1
|
||||
assert "SECRET123" not in captured.err
|
||||
assert "Unexpected error" in captured.err
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_error_message_readability_preserved(self, mock_fetch, capsys):
|
||||
"""Санитизация не ломает обычные сообщения об ошибках."""
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
mock_fetch.side_effect = RedmineAPIError("Authentication failed: bad key")
|
||||
code = main(["--date", "2026-01-01--2026-01-31"])
|
||||
captured = capsys.readouterr()
|
||||
assert code == 1
|
||||
assert "Authentication failed: bad key" in captured.err
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_debug_shows_full_traceback(self, mock_fetch, caplog):
|
||||
"""--debug выводит полный стектрейс исходного исключения."""
|
||||
import logging as _logging
|
||||
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
original = ValueError("raw details https://red.example.com?key=SECRET123")
|
||||
error = RedmineAPIError("safe message", original=original)
|
||||
error.__cause__ = original
|
||||
mock_fetch.side_effect = error
|
||||
with caplog.at_level(_logging.ERROR):
|
||||
code = main(["--date", "2026-01-01--2026-01-31", "--debug"])
|
||||
assert code == 1
|
||||
assert "raw details" in caplog.text
|
||||
assert "Traceback" in caplog.text
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_verbose_shows_full_traceback(self, mock_fetch, caplog):
|
||||
"""--verbose тоже даёт доступ к деталям/стектрейсу исходной ошибки."""
|
||||
import logging as _logging
|
||||
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
original = ValueError("raw details https://red.example.com?key=SECRET123")
|
||||
error = RedmineAPIError("safe message", original=original)
|
||||
error.__cause__ = original
|
||||
mock_fetch.side_effect = error
|
||||
with caplog.at_level(_logging.INFO):
|
||||
code = main(["--date", "2026-01-01--2026-01-31", "--verbose"])
|
||||
assert code == 1
|
||||
assert "raw details" in caplog.text
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_no_flags_hides_original_details(self, mock_fetch, caplog, capsys):
|
||||
"""Без флагов детали исходного исключения не печатаются."""
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
original = ValueError("raw details https://red.example.com?key=SECRET123")
|
||||
mock_fetch.side_effect = RedmineAPIError("safe message", original=original)
|
||||
code = main(["--date", "2026-01-01--2026-01-31"])
|
||||
captured = capsys.readouterr()
|
||||
assert code == 1
|
||||
assert "raw details" not in captured.err
|
||||
assert "raw details" not in caplog.text
|
||||
assert "safe message" in captured.err
|
||||
|
||||
Reference in New Issue
Block a user