import os from unittest import mock import pytest from redmine_reporter.cli import main, parse_date_range VALID_ENV = { "REDMINE_URL": "https://red.eltex.loc", "REDMINE_API_KEY": "token", } @pytest.fixture(autouse=True) def _reset_config_overrides(): """Сбрасывает CLI-переопределения Config перед каждым тестом.""" from redmine_reporter.config import Config Config.set_redmine_url("") Config.set_redmine_api_key("") yield Config.set_redmine_url("") Config.set_redmine_api_key("") @pytest.mark.parametrize( "date_arg, expected", [ ("2026-01-01--2026-01-31", ("2026-01-01", "2026-01-31")), (" 2026-01-01 -- 2026-01-31 ", ("2026-01-01", "2026-01-31")), ], ) def test_parse_date_range_valid(date_arg, expected): assert parse_date_range(date_arg) == expected @pytest.mark.parametrize( "date_arg", [ "20260101-20260131", "2026-1-01--2026-01-31", "2026-02-30--2026-03-01", "2026-02-01--2026-01-31", ], ) def test_parse_date_range_invalid(date_arg): with pytest.raises(ValueError): parse_date_range(date_arg) @mock.patch.dict(os.environ, VALID_ENV, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_cli_returns_zero_on_no_entries(mock_fetch): """None от fetch (нет time entries) -- выход 0.""" mock_fetch.return_value = None code = main(["--date", "2026-01-01--2026-01-31"]) assert code == 0 @mock.patch.dict(os.environ, {}, clear=True) def test_cli_config_error(): """Невалидный конфиг -- выход 1.""" code = main(["--date", "2026-01-01--2026-01-31"]) assert code == 1 @mock.patch.dict(os.environ, VALID_ENV, clear=True) def test_cli_invalid_date_format(): """Неверный формат даты -- выход 1.""" code = main(["--date", "20260101-20260131"]) assert code == 1 class _MockIssue: """Простой mock Redmine Issue для CLI-тестов.""" def __init__(self, issue_id=1, subject="Task", project="Project", status="New"): self.id = issue_id self.subject = subject self.project = project self.status = status self.fixed_version = None @mock.patch.dict(os.environ, VALID_ENV, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_cli_unknown_output_extension(mock_fetch, tmp_path): """Неизвестное расширение файла -- выход 1.""" issue = _MockIssue() mock_fetch.return_value = [(issue, 1.0)] output = str(tmp_path / "report.xyz") code = main(["--date", "2026-01-01--2026-01-31", "--output", output]) assert code == 1 @mock.patch.dict(os.environ, VALID_ENV, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_cli_output_without_extension(mock_fetch, tmp_path): """Файл без расширения -- выход 1 с подсказкой.""" issue = _MockIssue() mock_fetch.return_value = [(issue, 1.0)] output = str(tmp_path / "report") code = main(["--date", "2026-01-01--2026-01-31", "--output", output]) assert code == 1 @mock.patch.dict(os.environ, VALID_ENV, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") @mock.patch("redmine_reporter.cli.get_formatter_by_extension") def test_cli_odt_missing_odfpy_message(mock_gf, mock_fetch, tmp_path, capsys): """При запросе .odt без odfpy — выход 1, понятное сообщение про odfpy.""" issue = _MockIssue() mock_fetch.return_value = [(issue, 1.0)] mock_gf.return_value = None output = str(tmp_path / "report.odt") code = main(["--date", "2026-01-01--2026-01-31", "--output", output]) assert code == 1 captured = capsys.readouterr() assert "odfpy" in captured.err @mock.patch.dict(os.environ, VALID_ENV, clear=True) def test_cli_version_flag(capsys): """--version выводит версию и завершается с 0.""" with pytest.raises(SystemExit) as exc_info: main(["--version"]) assert exc_info.value.code == 0 captured = capsys.readouterr() assert "redmine-reporter" in captured.out from redmine_reporter import __version__ assert __version__ in captured.out @mock.patch.dict(os.environ, VALID_ENV, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_cli_verbose_and_debug_flags_accepted(mock_fetch): """--verbose и --debug не ломают запуск.""" mock_fetch.return_value = None code = main(["--date", "2026-01-01--2026-01-31", "--verbose"]) assert code == 0 code = main(["--date", "2026-01-01--2026-01-31", "--debug"]) assert code == 0 class _MockIssue: """Простой mock Redmine Issue для CLI-тестов.""" def __init__(self, issue_id=1, subject="Task", project="Project", status="New"): self.id = issue_id self.subject = subject self.project = project self.status = status self.fixed_version = None @mock.patch.dict(os.environ, {}, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_cli_url_and_api_key_override_env(mock_fetch): """--url и --api-key переопределяют отсутствующие/другие env-переменные.""" mock_fetch.return_value = None code = main( [ "--date", "2026-01-01--2026-01-31", "--url", "https://other.redmine.loc", "--api-key", "cli-token", ] ) assert code == 0 @mock.patch.dict(os.environ, {}, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_cli_config_file_loading(mock_fetch, tmp_path): """--config загружает переменные из указанного .env-файла.""" config_path = tmp_path / "custom.env" config_path.write_text( "REDMINE_URL=https://config.redmine.loc\n" "REDMINE_API_KEY=config-token\n", encoding="utf-8", ) mock_fetch.return_value = None code = main(["--date", "2026-01-01--2026-01-31", "--config", str(config_path)]) assert code == 0 @mock.patch.dict(os.environ, VALID_ENV, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_cli_summary_flag_prints_totals(mock_fetch, capsys): """--summary выводит общее время и разбивку по проектам в stderr.""" issue = _MockIssue() mock_fetch.return_value = [(issue, 1.0)] main(["--date", "2026-01-01--2026-01-31", "--summary"]) captured = capsys.readouterr() assert "Total time" in captured.err assert "Project" in captured.err or "Project" in captured.out @mock.patch.dict(os.environ, VALID_ENV, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_total_issues_message_goes_to_stderr(mock_fetch, capsys): """«Total issues» пишется в stderr, не загрязняя stdout при pipe (#28).""" issue = _MockIssue() mock_fetch.return_value = [(issue, 1.0)] main(["--date", "2026-01-01--2026-01-31"]) captured = capsys.readouterr() assert "Total issues" not in captured.out assert "Total issues" in captured.err @mock.patch.dict(os.environ, VALID_ENV, clear=True) @mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time") def test_cli_no_time_passed_to_formatter(mock_fetch, tmp_path): """CLI --no-time передаётся в файловый форматтер.""" issue = _MockIssue() mock_fetch.return_value = [(issue, 1.0)] output = str(tmp_path / "report.xlsx") with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get_formatter: mock_formatter = mock.MagicMock() mock_get_formatter.return_value = mock_formatter main(["--date", "2026-01-01--2026-01-31", "--output", output, "--no-time"]) _, kwargs = mock_get_formatter.call_args assert kwargs.get("no_time") is True