Merge branch 'fix/55-cli-sanitize'

This commit is contained in:
Кокос Артем Николаевич
2026-07-17 12:57:43 +07:00
2 changed files with 142 additions and 4 deletions

View File

@@ -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